Skip to content

Instantly share code, notes, and snippets.

@syuilo
Last active April 5, 2020 05:09
Show Gist options
  • Save syuilo/5ce265ddd79732d8976b84d794f9000b to your computer and use it in GitHub Desktop.
Save syuilo/5ce265ddd79732d8976b84d794f9000b to your computer and use it in GitHub Desktop.
/**
* 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