Last active
April 5, 2020 05:09
-
-
Save syuilo/5ce265ddd79732d8976b84d794f9000b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* AiScript interpreter | |
*/ | |
import autobind from 'autobind-decorator'; | |
type ValueType = 'boolean' | 'number' | 'string' | 'object' | 'array' | 'function'; | |
type VNull = { | |
type: 'null'; | |
value: null; | |
}; | |
type VBoolean = { | |
type: 'boolean'; | |
value: boolean; | |
}; | |
type VNumber = { | |
type: 'number'; | |
value: number; | |
}; | |
type VString = { | |
type: 'string'; | |
value: string; | |
}; | |
type VFunction = { | |
type: 'function'; | |
args?: string[]; | |
statements?: Node[]; | |
native?: (args: Value[]) => Value | void; | |
}; | |
type Value = VNull | VBoolean | VNumber | VString | VFunction; | |
type Node = { | |
type: 'defineVariable'; // 変数宣言 | |
name: string; // 変数名 | |
expression: Node; // 式 | |
} | { | |
type: 'call'; // 関数呼び出し | |
name: string; // 関数名 | |
arguments: Node[]; // 引数(式の配列) | |
} | { | |
type: 'return'; // return | |
expression: Node; // 式 | |
} | { | |
type: 'if'; // if文 | |
cond: Node; // 条件式 | |
then: Node[]; // if文のthen節 | |
elseif: { | |
cond: Node; | |
then: Node[]; | |
}[]; // if文のelseif節 | |
else: Node[]; // if文のelse節 | |
} | { | |
type: 'variable'; // 変数 | |
name: string; // 変数名 | |
} | { | |
type: 'null'; // nullリテラル | |
value: null; // null | |
} | { | |
type: 'boolean'; // 真理値リテラル | |
value: boolean; // 真理値 | |
} | { | |
type: 'number'; // 数値リテラル | |
value: number; // 数値 | |
} | { | |
type: 'string'; // 文字列リテラル | |
value: string; // 文字列 | |
} | { | |
type: 'function'; // 関数リテラル | |
arguments: string[]; // 引数名 | |
children: Node[]; // 関数の本体処理 | |
} | { | |
type: 'object'; // オブジェクトリテラル | |
object: { | |
key: string; // キー | |
value: Node; // バリュー(式) | |
}[]; // オブジェクト | |
}; | |
const NULL = { | |
type: 'null' as const, | |
value: null | |
}; | |
export const std: Record<string, Value> = { | |
print: { | |
type: 'function', | |
native(args) { | |
console.log(args[0]); | |
} | |
}, | |
add: { | |
type: 'function', | |
native(args) { | |
return { | |
type: 'number', | |
value: args[0].value + args[1].value | |
}; | |
} | |
}, | |
}; | |
class AiScriptError extends Error { | |
public info?: any; | |
constructor(message: string, info?: any) { | |
super(message); | |
this.info = info; | |
// 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, Value>[]; | |
public name: string; | |
constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) { | |
this.layerdStates = layerdStates; | |
this.name = name || (layerdStates.length === 1 ? '<root>' : '<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 get(name: string): Value { | |
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}'`, { | |
scope: this.layerdStates | |
}); | |
} | |
/** | |
* 指定した名前の変数を現在のスコープに追加します | |
* @param name 変数名 | |
*/ | |
@autobind | |
public add(name: string, val: Value) { | |
this.layerdStates[0][name] = val; | |
} | |
} | |
function valToString(val: Value) { | |
const label = | |
val.type === 'number' ? val.value : | |
val.type === 'boolean' ? val.value : | |
val.type === 'string' ? val.value : | |
val.type === 'function' ? null : | |
val.type === 'null' ? null : | |
null; | |
return label ? `${val.type} (${label})` : val.type; | |
} | |
function nodeToString(node: Node) { | |
const label = | |
node.type === 'number' ? node.value : | |
node.type === 'boolean' ? node.value : | |
node.type === 'string' ? node.value : | |
node.type === 'function' ? null : | |
node.type === 'null' ? null : | |
node.type === 'defineVariable' ? node.name : | |
node.type === 'variable' ? node.name : | |
node.type === 'call' ? node.name : | |
null; | |
return label ? `${node.type} (${label})` : node.type; | |
} | |
export function run(script: Node[], vars: Record<string, Value>, maxStep: number = 1000) { | |
let steps = 0; | |
const scope = new Scope([vars]); | |
function evalExp(node: Node, scope: Scope): Value { | |
console.log(`+ ${nodeToString(node)}`); | |
if (node.type === 'call') { | |
const val = scope.get(node.name); | |
if (val.type !== 'function') throw new AiScriptError(`#${node.name} is not a function (${val.type})`); | |
if (val.native) { | |
const result = val.native!(node.arguments.map(argument => evalExp(argument, scope))); | |
return result || NULL; | |
} else { | |
const args = {} as Record<string, any>; | |
for (let i = 0; i < val.args!.length; i++) { | |
args[val.args![i]] = evalExp(node.arguments[i], scope); | |
} | |
const fnScope = scope.createChildScope(args, `#${node.name}`); | |
return evalFn(val.statements!, fnScope); | |
} | |
} else if (node.type === 'if') { | |
} else if (node.type === 'defineVariable') { | |
scope.add(node.name, evalExp(node.expression, scope)); | |
return NULL; | |
} else if (node.type === 'variable') { | |
return scope.get(node.name); | |
} else if (node.type === 'number') { | |
return { | |
type: 'number', | |
value: node.value | |
}; | |
} else if (node.type === 'string') { | |
return { | |
type: 'string', | |
value: node.value | |
}; | |
} else if (node.type === 'function') { | |
return { | |
type: 'function', | |
args: node.arguments!, | |
statements: node.children!, | |
}; | |
} else { | |
throw new Error('unknown ast type: ' + node.type); | |
} | |
} | |
function evalFn(program: Node[], scope: Scope): Value { | |
console.log(`-> ${scope.name}`); | |
for (let i = 0; i < program.length; i++) { | |
const node = program[i]; | |
if (node.type === 'return') { | |
const val = evalExp(node.expression, scope); | |
console.log(`<- ${scope.name} : ${valToString(val)}`); | |
return val; | |
} else { | |
evalExp(node, scope); | |
} | |
} | |
// 最後までreturnが無かったらnullを返す | |
return NULL; | |
} | |
return evalFn(script, scope); | |
} | |
console.log('>>> ' + valToString(run([ | |
{ | |
type: 'defineVariable', | |
name: 'func1', | |
expression: { | |
type: 'function', | |
arguments: ['x'], | |
children: [ | |
{ | |
type: 'return', | |
expression: { | |
type: 'call', | |
name: 'add', | |
arguments: [ | |
{ | |
type: 'variable', | |
name: 'x' | |
}, | |
{ | |
type: 'number', | |
value: 1 | |
} | |
], | |
} | |
} | |
] | |
} | |
}, | |
{ | |
type: 'return', | |
expression: { | |
type: 'call', | |
name: 'func1', | |
arguments: [ | |
{ | |
type: 'number', | |
value: 2 | |
} | |
] | |
} | |
} | |
] as any, std))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment