diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index ad73857f..39f2d71e 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -645,6 +645,7 @@ export class Scope { exists(name: string): boolean; get(name: string): Value; getAll(): Map; + getNsPrefix(): string; // (undocumented) name: string; // (undocumented) @@ -857,6 +858,7 @@ type VStr = { // @public (undocumented) type VUserFn = VFnBase & { native?: undefined; + name?: string; args: VFnArg[]; statements: Node_2[]; scope: Scope; @@ -864,8 +866,8 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:38:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts -// src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:43: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/interpreter/index.ts b/src/interpreter/index.ts index 33520fe8..2aaae5b3 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -7,7 +7,7 @@ import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexO import * as Ast from '../node.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; -import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue } from './util.js'; +import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; @@ -20,6 +20,11 @@ export type LogObject = { val?: Value | Variable; }; +type CallInfo = { + name: string; + pos: Ast.Pos | undefined; +}; + export class Interpreter { public stepCount = 0; private stop = false; @@ -97,7 +102,7 @@ export class Interpreter { if (script == null || script.length === 0) return; try { await this.collectNs(script); - const result = await this._run(script, this.scope); + const result = await this._run(script, this.scope, []); this.log('end', { val: result }); } catch (e) { this.handleError(e); @@ -114,7 +119,7 @@ export class Interpreter { */ @autobind public async execFn(fn: VFn, args: Value[]): Promise { - return await this._fn(fn, args) + return await this._fn(fn, args, []) .catch(e => { this.handleError(e); return ERROR('func_failed'); @@ -128,7 +133,7 @@ export class Interpreter { */ @autobind public execFnSimple(fn: VFn, args: Value[]): Promise { - return this._fn(fn, args); + return this._fn(fn, args, []); } @autobind @@ -224,7 +229,14 @@ export class Interpreter { throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.dest.name, node.loc.start); } - const value = await this._eval(node.expr, nsScope); + const value = await this._eval(node.expr, nsScope, []); + if ( + node.expr.type === 'fn' + && isFunction(value) + && !value.native + ) { + value.name = nsScope.getNsPrefix() + node.dest.name; + } await this.define(nsScope, node.dest, value, node.mut); break; @@ -245,10 +257,11 @@ export class Interpreter { } @autobind - private async _fn(fn: VFn, args: Value[]): Promise { + private async _fn(fn: VFn, args: Value[], callStack: readonly CallInfo[], pos?: Ast.Pos): Promise { if (fn.native) { + const info: CallInfo = { name: '', pos }; const result = fn.native(args, { - call: this.execFnSimple, + call: (fn, args) => this._fn(fn, args, [...callStack, info]), topCall: this.execFn, registerAbortHandler: this.registerAbortHandler, unregisterAbortHandler: this.unregisterAbortHandler, @@ -262,30 +275,40 @@ export class Interpreter { if (!argdef.default) expectAny(args[i]); this.define(fnScope, argdef.dest, args[i] ?? argdef.default!, true); } - return unWrapRet(await this._run(fn.statements!, fnScope)); + + const info: CallInfo = { name: fn.name ?? '', pos }; + return unWrapRet(await this._run(fn.statements!, fnScope, [...callStack, info])); } } @autobind - private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope): Promise { - return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope); + private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope, callStack); } @autobind - private _eval(node: Ast.Node, scope: Scope): Promise { - return this.__eval(node, scope).catch(e => { + private _eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { + return this.__eval(node, scope, callStack).catch(e => { if (e.pos) throw e; else { const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e); e2.pos = node.loc.start; - e2.message = `${e2.message} (Line ${e2.pos.line}, Column ${e2.pos.column})`; + e2.message = [ + e2.message, + ...[...callStack, { pos: e2.pos }].map(({ pos }, i) => { + const name = callStack[i - 1]?.name ?? ''; + return pos + ? ` at ${name} (Line ${pos.line}, Column ${pos.column})` + : ` at ${name}`; + }).reverse(), + ].join('\n'); throw e2; } }); } @autobind - private async __eval(node: Ast.Node, scope: Scope): Promise { + private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { if (this.stop) return NULL; // irqRateが小数の場合は不等間隔になる if (this.irqRate !== 0 && this.stepCount % this.irqRate >= this.irqRate - 1) { @@ -298,46 +321,41 @@ export class Interpreter { switch (node.type) { case 'call': { - const callee = await this._eval(node.target, scope); + const callee = await this._eval(node.target, scope, callStack); assertFunction(callee); - const args = await Promise.all(node.args.map(expr => this._eval(expr, scope))); - return this._fn(callee, args); + const args = await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack))); + return this._fn(callee, args, callStack, node.loc.start); } case 'if': { - const cond = await this._eval(node.cond, scope); + const cond = await this._eval(node.cond, scope, callStack); assertBoolean(cond); if (cond.value) { - return this._evalClause(node.then, scope); - } else { - if (node.elseif && node.elseif.length > 0) { - for (const elseif of node.elseif) { - const cond = await this._eval(elseif.cond, scope); - assertBoolean(cond); - if (cond.value) { - return this._evalClause(elseif.then, scope); - } - } - if (node.else) { - return this._eval(node.else, scope); - } - } else if (node.else) { - return this._evalClause(node.else, scope); + return this._evalClause(node.then, scope, callStack); + } + for (const elseif of node.elseif) { + const cond = await this._eval(elseif.cond, scope, callStack); + assertBoolean(cond); + if (cond.value) { + return this._evalClause(elseif.then, scope, callStack); } } + if (node.else) { + return this._evalClause(node.else, scope, callStack); + } return NULL; } case 'match': { - const about = await this._eval(node.about, scope); + const about = await this._eval(node.about, scope, callStack); for (const qa of node.qs) { - const q = await this._eval(qa.q, scope); + const q = await this._eval(qa.q, scope, callStack); if (eq(about, q)) { - return await this._evalClause(qa.a, scope); + return await this._evalClause(qa.a, scope, callStack); } } if (node.default) { - return await this._evalClause(node.default, scope); + return await this._evalClause(node.default, scope, callStack); } return NULL; } @@ -345,7 +363,7 @@ export class Interpreter { case 'loop': { // eslint-disable-next-line no-constant-condition while (true) { - const v = await this._run(node.statements, scope.createChildScope()); + const v = await this._run(node.statements, scope.createChildScope(), callStack); if (v.type === 'break') { break; } else if (v.type === 'return') { @@ -357,10 +375,10 @@ export class Interpreter { case 'for': { if (node.times) { - const times = await this._eval(node.times, scope); + const times = await this._eval(node.times, scope, callStack); assertNumber(times); for (let i = 0; i < times.value; i++) { - const v = await this._evalClause(node.for, scope); + const v = await this._evalClause(node.for, scope, callStack); if (v.type === 'break') { break; } else if (v.type === 'return') { @@ -368,8 +386,8 @@ export class Interpreter { } } } else { - const from = await this._eval(node.from!, scope); - const to = await this._eval(node.to!, scope); + const from = await this._eval(node.from!, scope, callStack); + const to = await this._eval(node.to!, scope, callStack); assertNumber(from); assertNumber(to); for (let i = from.value; i < from.value + to.value; i++) { @@ -378,7 +396,7 @@ export class Interpreter { isMutable: false, value: NUM(i), }], - ]))); + ])), callStack); if (v.type === 'break') { break; } else if (v.type === 'return') { @@ -390,12 +408,12 @@ export class Interpreter { } case 'each': { - const items = await this._eval(node.items, scope); + const items = await this._eval(node.items, scope, callStack); assertArray(items); for (const item of items.value) { const eachScope = scope.createChildScope(); this.define(eachScope, node.var, item, false); - const v = await this._eval(node.for, eachScope); + const v = await this._eval(node.for, eachScope, callStack); if (v.type === 'break') { break; } else if (v.type === 'return') { @@ -406,17 +424,25 @@ export class Interpreter { } case 'def': { - const value = await this._eval(node.expr, scope); + const value = await this._eval(node.expr, scope, callStack); if (node.attr.length > 0) { const attrs: Value['attr'] = []; for (const nAttr of node.attr) { attrs.push({ name: nAttr.name, - value: await this._eval(nAttr.value, scope), + value: await this._eval(nAttr.value, scope, callStack), }); } value.attr = attrs; } + if ( + node.expr.type === 'fn' + && node.dest.type === 'identifier' + && isFunction(value) + && !value.native + ) { + value.name = node.dest.name; + } await this.define(scope, node.dest, value, node.mut); return NULL; } @@ -426,30 +452,30 @@ export class Interpreter { } case 'assign': { - const v = await this._eval(node.expr, scope); + const v = await this._eval(node.expr, scope, callStack); - await this.assign(scope, node.dest, v); + await this.assign(scope, node.dest, v, callStack); return NULL; } case 'addAssign': { - const target = await this._eval(node.dest, scope); + const target = await this._eval(node.dest, scope, callStack); assertNumber(target); - const v = await this._eval(node.expr, scope); + const v = await this._eval(node.expr, scope, callStack); assertNumber(v); - await this.assign(scope, node.dest, NUM(target.value + v.value)); + await this.assign(scope, node.dest, NUM(target.value + v.value), callStack); return NULL; } case 'subAssign': { - const target = await this._eval(node.dest, scope); + const target = await this._eval(node.dest, scope, callStack); assertNumber(target); - const v = await this._eval(node.expr, scope); + const v = await this._eval(node.expr, scope, callStack); assertNumber(v); - await this.assign(scope, node.dest, NUM(target.value - v.value)); + await this.assign(scope, node.dest, NUM(target.value - v.value), callStack); return NULL; } @@ -461,18 +487,20 @@ export class Interpreter { case 'str': return STR(node.value); - case 'arr': return ARR(await Promise.all(node.value.map(item => this._eval(item, scope)))); + case 'arr': return ARR(await Promise.all( + node.value.map(item => this._eval(item, scope, callStack)) + )); case 'obj': { const obj = new Map(); for (const [key, value] of node.value) { - obj.set(key, await this._eval(value, scope)); + obj.set(key, await this._eval(value, scope, callStack)); } return OBJ(obj); } case 'prop': { - const target = await this._eval(node.target, scope); + const target = await this._eval(node.target, scope, callStack); if (isObject(target)) { if (target.value.has(node.name)) { return target.value.get(node.name)!; @@ -485,8 +513,8 @@ export class Interpreter { } case 'index': { - const target = await this._eval(node.target, scope); - const i = await this._eval(node.index, scope); + const target = await this._eval(node.target, scope, callStack); + const i = await this._eval(node.index, scope, callStack); if (isArray(target)) { assertNumber(i); const item = target.value[i.value]; @@ -507,7 +535,7 @@ export class Interpreter { } case 'not': { - const v = await this._eval(node.expr, scope); + const v = await this._eval(node.expr, scope, callStack); assertBoolean(v); return BOOL(!v.value); } @@ -517,7 +545,10 @@ export class Interpreter { await Promise.all(node.args.map(async (arg) => { return { dest: arg.dest, - default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined, + default: + arg.default ? await this._eval(arg.default, scope, callStack) : + arg.optional ? NULL : + undefined, // type: (TODO) }; })), @@ -527,7 +558,7 @@ export class Interpreter { } case 'block': { - return this._run(node.statements, scope.createChildScope()); + return this._run(node.statements, scope.createChildScope(), callStack); } case 'exists': { @@ -540,7 +571,7 @@ export class Interpreter { if (typeof x === 'string') { str += x; } else { - const v = await this._eval(x, scope); + const v = await this._eval(x, scope, callStack); str += reprValue(v); } } @@ -548,7 +579,7 @@ export class Interpreter { } case 'return': { - const val = await this._eval(node.expr, scope); + const val = await this._eval(node.expr, scope, callStack); this.log('block:return', { scope: scope.name, val: val }); return RETURN(val); } @@ -574,120 +605,120 @@ export class Interpreter { case 'pow': { const callee = scope.get('Core:pow'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'mul': { const callee = scope.get('Core:mul'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'div': { const callee = scope.get('Core:div'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'rem': { const callee = scope.get('Core:mod'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'add': { const callee = scope.get('Core:add'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'sub': { const callee = scope.get('Core:sub'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'lt': { const callee = scope.get('Core:lt'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'lteq': { const callee = scope.get('Core:lteq'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'gt': { const callee = scope.get('Core:gt'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'gteq': { const callee = scope.get('Core:gteq'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'eq': { const callee = scope.get('Core:eq'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'neq': { const callee = scope.get('Core:neq'); assertFunction(callee); - const left = await this._eval(node.left, scope); - const right = await this._eval(node.right, scope); - return this._fn(callee, [left, right]); + const left = await this._eval(node.left, scope, callStack); + const right = await this._eval(node.right, scope, callStack); + return this._fn(callee, [left, right], callStack); } case 'and': { - const leftValue = await this._eval(node.left, scope); + const leftValue = await this._eval(node.left, scope, callStack); assertBoolean(leftValue); if (!leftValue.value) { return leftValue; } else { - const rightValue = await this._eval(node.right, scope); + const rightValue = await this._eval(node.right, scope, callStack); assertBoolean(rightValue); return rightValue; } } case 'or': { - const leftValue = await this._eval(node.left, scope); + const leftValue = await this._eval(node.left, scope, callStack); assertBoolean(leftValue); if (leftValue.value) { return leftValue; } else { - const rightValue = await this._eval(node.right, scope); + const rightValue = await this._eval(node.right, scope, callStack); assertBoolean(rightValue); return rightValue; } @@ -700,7 +731,7 @@ export class Interpreter { } @autobind - private async _run(program: Ast.Node[], scope: Scope): Promise { + private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise { this.log('block:enter', { scope: scope.name }); let v: Value = NULL; @@ -708,7 +739,7 @@ export class Interpreter { for (let i = 0; i < program.length; i++) { const node = program[i]!; - v = await this._eval(node, scope); + v = await this._eval(node, scope, callStack); if (v.type === 'return') { this.log('block:return', { scope: scope.name, val: v.value }); return v; @@ -772,15 +803,20 @@ export class Interpreter { } @autobind - private async assign(scope: Scope, dest: Ast.Expression, value: Value): Promise { + private async assign( + scope: Scope, + dest: Ast.Expression, + value: Value, + callStack: readonly CallInfo[], + ): Promise { switch (dest.type) { case 'identifier': { scope.assign(dest.name, value); break; } case 'index': { - const assignee = await this._eval(dest.target, scope); - const i = await this._eval(dest.index, scope); + const assignee = await this._eval(dest.target, scope, callStack); + const i = await this._eval(dest.index, scope, callStack); if (isArray(assignee)) { assertNumber(i); if (assignee.value[i.value] === undefined) { @@ -796,7 +832,7 @@ export class Interpreter { break; } case 'prop': { - const assignee = await this._eval(dest.target, scope); + const assignee = await this._eval(dest.target, scope, callStack); assertObject(assignee); assignee.value.set(dest.name, value); @@ -805,14 +841,14 @@ export class Interpreter { case 'arr': { assertArray(value); await Promise.all(dest.value.map( - (item, index) => this.assign(scope, item, value.value[index] ?? NULL), + (item, index) => this.assign(scope, item, value.value[index] ?? NULL, callStack), )); break; } case 'obj': { assertObject(value); await Promise.all([...dest.value].map( - ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL), + ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL, callStack), )); break; } diff --git a/src/interpreter/scope.ts b/src/interpreter/scope.ts index a4409638..34142551 100644 --- a/src/interpreter/scope.ts +++ b/src/interpreter/scope.ts @@ -70,6 +70,15 @@ export class Scope { { scope: this.layerdStates }); } + /** + * 名前空間名を取得します。 + */ + @autobind + public getNsPrefix(): string { + if (this.parent == null || this.nsName == null) return ''; + return this.parent.getNsPrefix() + this.nsName + ':'; + } + /** * 指定した名前の変数が存在するか判定します * @param name - 変数名 diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index a5d271a9..d1e64868 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -37,6 +37,7 @@ type VFnBase = { }; export type VUserFn = VFnBase & { native?: undefined; // if (vfn.native) で型アサーション出来るように + name?: string; args: VFnArg[]; statements: Node[]; scope: Scope; diff --git a/test/interpreter.ts b/test/interpreter.ts index 203b5a41..09cb5452 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -115,6 +115,56 @@ describe('error location', () => { }); }); +describe('callstack', () => { + const exeAndGetErrMessage = (src: string): Promise => new Promise((ok, ng) => { + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + }, { + err(e) { ok(e.message) }, + }); + aiscript.exec(Parser.parse(src)).then(() => ng('error has not occurred.')); + }); + + test('error in function', async () => { + const result = await exeAndGetErrMessage(` + @function1() { emitError() } + @function2() { function1() } + function2() + `); + expect(result).toMatchInlineSnapshot(` + "emitError + at function1 (Line 2, Column 28) + at function2 (Line 3, Column 28) + at (Line 4, Column 13)" + `); + }); + test('error in function in namespace', async () => { + const result = await exeAndGetErrMessage(` + :: Ai { + @function() { emitError() } + } + Ai:function() + `); + expect(result).toMatchInlineSnapshot(` + "emitError + at Ai:function (Line 3, Column 28) + at (Line 5, Column 15)" + `); + }); + test('error in anonymous function', async () => { + const result = await exeAndGetErrMessage(` + (@() { emitError() })() + `); + expect(result).toMatchInlineSnapshot(` + "emitError + at (Line 2, Column 20) + at (Line 2, Column 25)" + `); + }); +}); + describe('IRQ', () => { describe('irqSleep is function', () => { async function countSleeps(irqRate: number): Promise { diff --git a/unreleased/callstack.md b/unreleased/callstack.md new file mode 100644 index 00000000..c7ec3085 --- /dev/null +++ b/unreleased/callstack.md @@ -0,0 +1 @@ +- ランタイムエラーにコールスタックの情報を追加。