From 38ff523cb1d96cc751ee02afa248eebd6e9644c1 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Mon, 16 Oct 2023 00:32:00 +0900 Subject: [PATCH 1/3] add location to runtime errors --- etc/aiscript.api.md | 36 ++++++++++++++++++++++++++++-------- src/error.ts | 12 ++++++++++++ src/interpreter/index.ts | 26 ++++++++++++++------------ src/interpreter/value.ts | 22 ++++++++++++++-------- 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 82532fc2..5be088d7 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -27,6 +27,15 @@ class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError { constructor(message: string, info?: any); } +// @public +class AiScriptNamespaceError extends AiScriptError { + constructor(message: string, loc: Loc, info?: any); + // (undocumented) + loc: Loc; + // (undocumented) + name: string; +} + // @public class AiScriptRuntimeError extends AiScriptError { constructor(message: string, info?: any); @@ -219,6 +228,7 @@ declare namespace errors { NonAiScriptError, AiScriptSyntaxError, AiScriptTypeError, + AiScriptNamespaceError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } @@ -244,7 +254,7 @@ const FALSE: { }; // @public (undocumented) -const FN: (args: VFn['args'], statements: VFn['statements'], scope: VFn['scope']) => VFn; +const FN: (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']) => VUserFn; // @public (undocumented) type Fn = NodeBase & { @@ -258,7 +268,7 @@ type Fn = NodeBase & { }; // @public (undocumented) -const FN_NATIVE: (fn: VFn['native']) => VFn; +const FN_NATIVE: (fn: VNativeFn['native']) => VNativeFn; // @public (undocumented) type FnTypeSource = NodeBase & { @@ -591,6 +601,8 @@ declare namespace values { VArr, VObj, VFn, + VUserFn, + VNativeFn, VReturn, VBreak, VContinue, @@ -647,18 +659,17 @@ type VError = { info?: Value; }; +// @public (undocumented) +type VFn = VUserFn | VNativeFn; + // @public -type VFn = { - type: 'fn'; - args?: string[]; - statements?: Node_2[]; - native?: (args: (Value | undefined)[], opts: { +type VNativeFn = VFnBase & { + native: (args: (Value | undefined)[], opts: { call: (fn: VFn, args: Value[]) => Promise; topCall: (fn: VFn, args: Value[]) => Promise; registerAbortHandler: (handler: () => void) => void; unregisterAbortHandler: (handler: () => void) => void; }) => Value | Promise | void; - scope?: Scope; }; // @public (undocumented) @@ -690,6 +701,15 @@ type VStr = { value: string; }; +// Warning: (ae-forgotten-export) The symbol "VFnBase" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type VUserFn = VFnBase & { + native?: undefined; + statements: Node_2[]; + scope: Scope; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/src/error.ts b/src/error.ts index 0a7a0f35..693254a2 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,3 +1,5 @@ +import type { Loc } from './node.js'; + export abstract class AiScriptError extends Error { // name is read by Error.prototype.toString public name = 'AiScript'; @@ -44,6 +46,16 @@ export class AiScriptTypeError extends AiScriptError { } } +/** + * Namespace collection errors. + */ +export class AiScriptNamespaceError extends AiScriptError { + public name = 'Namespace'; + constructor(message: string, public loc: Loc, info?: any) { + super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); + } +} + /** * Interpret-time errors. */ diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 6b5524ae..bdd1e276 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -3,7 +3,7 @@ */ import { autobind } from '../utils/mini-autobind.js'; -import { AiScriptError, NonAiScriptError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.js'; +import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.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'; @@ -83,10 +83,6 @@ export class Interpreter { * (ii)Otherwise, just throws a error. * * @remarks This is the same function as that passed to AiScript NATIVE functions as opts.topCall. - * - * @param fn - the function - * @param args - arguments for the function - * @returns Return value of the function, or ERROR('func_failed') when the (i) condition above is fulfilled. */ @autobind public async execFn(fn: VFn, args: Value[]): Promise { @@ -101,10 +97,6 @@ export class Interpreter { * Almost same as execFn but when error occurs this always throws and never calls callback. * * @remarks This is the same function as that passed to AiScript NATIVE functions as opts.call. - * - * @param fn - the function - * @param args - arguments for the function - * @returns Return value of the function. */ @autobind public execFnSimple(fn: VFn, args: Value[]): Promise { @@ -198,7 +190,7 @@ export class Interpreter { switch (node.type) { case 'def': { if (node.mut) { - throw new Error('Namespaces cannot include mutable variable: ' + node.name); + throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.name, node.loc); } const variable: Variable = { @@ -216,7 +208,10 @@ export class Interpreter { } default: { - throw new Error('invalid ns member type: ' + (node as Ast.Node).type); + // exhaustiveness check + const n: never = node; + const nd = n as Ast.Node; + throw new AiScriptNamespaceError('invalid ns member type: ' + nd.type, nd.loc); } } } @@ -247,7 +242,14 @@ export class Interpreter { } @autobind - private async _eval(node: Ast.Node, scope: Scope): Promise { + private _eval(node: Ast.Node, scope: Scope): Promise { + return this.__eval(node, scope).catch(e => { + throw e.loc ? e : (e.loc = node.loc, e.message = `${e.message} (Line ${node.loc.line}, Column ${node.loc.column})`, e); + }); + } + + @autobind + private async __eval(node: Ast.Node, scope: Scope): Promise { if (this.stop) return NULL; if (this.stepCount % IRQ_RATE === IRQ_AT) await new Promise(resolve => setTimeout(resolve, 5)); this.stepCount++; diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 0933bc18..90c531b0 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -30,20 +30,26 @@ export type VObj = { value: Map; }; +export type VFn = VUserFn | VNativeFn; +type VFnBase = { + type: 'fn'; + args?: string[]; +}; +export type VUserFn = VFnBase & { + native?: undefined; // if (vfn.native) で型アサーション出来るように + statements: Node[]; + scope: Scope; +}; /** * When your AiScript NATIVE function passes VFn.call to other caller(s) whose error thrown outside the scope, use VFn.topCall instead to keep it under AiScript error control system. */ -export type VFn = { - type: 'fn'; - args?: string[]; - statements?: Node[]; - native?: (args: (Value | undefined)[], opts: { +export type VNativeFn = VFnBase & { + native: (args: (Value | undefined)[], opts: { call: (fn: VFn, args: Value[]) => Promise; topCall: (fn: VFn, args: Value[]) => Promise; registerAbortHandler: (handler: () => void) => void; unregisterAbortHandler: (handler: () => void) => void; }) => Value | Promise | void; - scope?: Scope; }; export type VReturn = { @@ -115,14 +121,14 @@ export const ARR = (arr: VArr['value']): VArr => ({ value: arr, }); -export const FN = (args: VFn['args'], statements: VFn['statements'], scope: VFn['scope']): VFn => ({ +export const FN = (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({ type: 'fn' as const, args: args, statements: statements, scope: scope, }); -export const FN_NATIVE = (fn: VFn['native']): VFn => ({ +export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ type: 'fn' as const, native: fn, }); From 7139aaed58dc0373d3c02c70a613613423257566 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Sat, 21 Oct 2023 14:58:37 +0900 Subject: [PATCH 2/3] fix Non-aiscript error loc & add test --- src/error.ts | 1 + src/interpreter/index.ts | 8 ++- test/index.ts | 62 --------------------- test/interpreter.ts | 115 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 63 deletions(-) create mode 100644 test/interpreter.ts diff --git a/src/error.ts b/src/error.ts index d719e426..da6e68af 100644 --- a/src/error.ts +++ b/src/error.ts @@ -4,6 +4,7 @@ export abstract class AiScriptError extends Error { // name is read by Error.prototype.toString public name = 'AiScript'; public info?: any; + public loc?: Loc; constructor(message: string, info?: any) { super(message); diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index bdd1e276..3b5a5538 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -244,7 +244,13 @@ export class Interpreter { @autobind private _eval(node: Ast.Node, scope: Scope): Promise { return this.__eval(node, scope).catch(e => { - throw e.loc ? e : (e.loc = node.loc, e.message = `${e.message} (Line ${node.loc.line}, Column ${node.loc.column})`, e); + if (e.loc) throw e; + else { + const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e); + e2.loc = node.loc; + e2.message = `${e2.message} (Line ${node.loc.line}, Column ${node.loc.column})`; + throw e2; + } }); } diff --git a/test/index.ts b/test/index.ts index 48cb8a6b..47a1ed7b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -50,68 +50,6 @@ test.concurrent('empty script', async () => { assert.deepEqual(ast, []); }); -describe('Interpreter', () => { - describe('Scope', () => { - test.concurrent('getAll', async () => { - const aiscript = new Interpreter({}); - await aiscript.exec(Parser.parse(` - let a = 1 - @b() { - let x = a + 1 - x - } - if true { - var y = 2 - } - var c = true - `)); - const vars = aiscript.scope.getAll(); - assert.ok(vars.get('a') != null); - assert.ok(vars.get('b') != null); - assert.ok(vars.get('c') != null); - assert.ok(vars.get('x') == null); - assert.ok(vars.get('y') == null); - }); - }); -}); - -describe('error handler', () => { - test.concurrent('error from outside caller', async () => { - let outsideCaller: () => Promise = async () => {}; - let errCount: number = 0; - const aiscript = new Interpreter({ - emitError: FN_NATIVE((_args, _opts) => { - throw Error('emitError'); - }), - genOutsideCaller: FN_NATIVE(([fn], opts) => { - utils.assertFunction(fn); - outsideCaller = async () => { - opts.topCall(fn, []); - }; - }), - }, { - err(e) { errCount++ }, - }); - await aiscript.exec(Parser.parse(` - genOutsideCaller(emitError) - `)); - assert.strictEqual(errCount, 0); - await outsideCaller(); - assert.strictEqual(errCount, 1); - }); - - test.concurrent('array.map calls the handler just once', async () => { - let errCount: number = 0; - const aiscript = new Interpreter({}, { - err(e) { errCount++ }, - }); - await aiscript.exec(Parser.parse(` - Core:range(1,5).map(@(){ hoge }) - `)); - assert.strictEqual(errCount, 1); - }); -}); - describe('ops', () => { test.concurrent('==', async () => { eq(await exe('<: (1 == 1)'), BOOL(true)); diff --git a/test/interpreter.ts b/test/interpreter.ts new file mode 100644 index 00000000..f9953705 --- /dev/null +++ b/test/interpreter.ts @@ -0,0 +1,115 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { Parser, Interpreter, values, errors, utils } from '../src'; +let { FN_NATIVE } = values; +let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; + +describe('Scope', () => { + test.concurrent('getAll', async () => { + const aiscript = new Interpreter({}); + await aiscript.exec(Parser.parse(` + let a = 1 + @b() { + let x = a + 1 + x + } + if true { + var y = 2 + } + var c = true + `)); + const vars = aiscript.scope.getAll(); + assert.ok(vars.get('a') != null); + assert.ok(vars.get('b') != null); + assert.ok(vars.get('c') != null); + assert.ok(vars.get('x') == null); + assert.ok(vars.get('y') == null); + }); +}); + +describe('error handler', () => { + test.concurrent('error from outside caller', async () => { + let outsideCaller: () => Promise = async () => {}; + let errCount: number = 0; + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + genOutsideCaller: FN_NATIVE(([fn], opts) => { + utils.assertFunction(fn); + outsideCaller = async () => { + opts.topCall(fn, []); + }; + }), + }, { + err(e) { /*console.log(e.toString());*/ errCount++ }, + }); + await aiscript.exec(Parser.parse(` + genOutsideCaller(emitError) + `)); + assert.strictEqual(errCount, 0); + await outsideCaller(); + assert.strictEqual(errCount, 1); + }); + + test.concurrent('array.map calls the handler just once', async () => { + let errCount: number = 0; + const aiscript = new Interpreter({}, { + err(e) { errCount++ }, + }); + await aiscript.exec(Parser.parse(` + Core:range(1,5).map(@(){ hoge }) + `)); + assert.strictEqual(errCount, 1); + }); +}); + +describe('error location', () => { + const exeAndGetErrLoc = (src: string): Promise => new Promise((ok, ng) => { + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + }, { + err(e) { ok(e.loc) }, + }); + aiscript.exec(Parser.parse(src)).then(() => ng('error has not occured.')); + }); + + test.concurrent('Non-aiscript Error', async () => { + return expect(exeAndGetErrLoc(`/* (の位置 + */ + emitError() + `)).resolves.toEqual({ line: 3, column: 13}); + }); + + test.concurrent('No "var" in namespace declaration', async () => { + return expect(exeAndGetErrLoc(`// vの位置 + :: Ai { + let chan = 'kawaii' + var kun = '!?' + } + `)).resolves.toEqual({ line: 4, column: 5}); + }); + + test.concurrent('Index out of range', async () => { + return expect(exeAndGetErrLoc(`// [の位置 + let arr = [] + arr[0] + `)).resolves.toEqual({ line: 3, column: 7}); + }); + + test.concurrent('Error in passed function', async () => { + return expect(exeAndGetErrLoc(`// /の位置 + [0, 1, 2].map(@(v){ + 0/v + }) + `)).resolves.toEqual({ line: 3, column: 6}); + }); + + test.concurrent('No such prop', async () => { + return expect(exeAndGetErrLoc(`// .の位置 + [].ai + `)).resolves.toEqual({ line: 2, column: 6}); + }); +}); From 4def082537cb0770890178d7f1e2a2c218c642e2 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Sat, 21 Oct 2023 15:02:28 +0900 Subject: [PATCH 3/3] api --- etc/aiscript.api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 957149c2..d89418c6 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -19,6 +19,8 @@ abstract class AiScriptError extends Error { // (undocumented) info?: any; // (undocumented) + loc?: Loc; + // (undocumented) name: string; }