Improve AiScript

This commit is contained in:
syuilo 2019-05-01 14:54:34 +09:00
parent 2389ad7602
commit 64b6798651
6 changed files with 80 additions and 75 deletions

View File

@ -1853,6 +1853,7 @@ pages:
variables-info: "変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。"
variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。"
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: "入力スロット"

View File

@ -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<string, any>[];
public name: string;
constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) {
this.layerdStates = layerdStates;
this.name = name || 'anonymous';
}
@autobind
public createChildScope(states: Record<string, any>, 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<string, any> {
const values: Record<string, any> = {};
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<string, any> = {}): 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

View File

@ -25,6 +25,9 @@
<section v-else-if="value.type === 'ref'" class="hpdwcrvs">
<select v-model="value.value">
<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
<optgroup :label="$t('script.argVariables')">
<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
</optgroup>
<optgroup :label="$t('script.pageVariables')">
<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
</optgroup>
@ -33,11 +36,6 @@
</optgroup>
</select>
</section>
<section v-else-if="value.type === 'in'" class="hpdwcrvs">
<select v-model="value.value">
<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
</select>
</section>
<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
<ui-textarea v-model="slots">
<span>{{ $t('script.blocks._fn.slots') }}</span>
@ -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}`);
},
},

View File

@ -77,6 +77,7 @@
<template v-if="moreDetails">
<ui-info><span v-html="$t('variables-info2')"></span></ui-info>
<ui-info><span v-html="$t('variables-info3')"></span></ui-info>
<ui-info><span v-html="$t('variables-info4')"></span></ui-info>
</template>
</div>
</ui-container>

View File

@ -1,5 +1,5 @@
<template>
<div v-show="script.vars.find(x => x.name === value.var).value">
<div v-show="script.vars[value.var]">
<x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h"/>
</div>
</template>

View File

@ -27,7 +27,7 @@ import { url } from '../../../../config';
class Script {
public aiScript: AiScript;
public vars: any;
public vars: Record<string, any>;
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();
});
}