Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ abstract class AiScriptError extends Error {
// (undocumented)
info?: any;
// (undocumented)
loc?: Loc;
// (undocumented)
name: string;
}

Expand All @@ -27,6 +29,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);
Expand Down Expand Up @@ -223,6 +234,7 @@ declare namespace errors {
NonAiScriptError,
AiScriptSyntaxError,
AiScriptTypeError,
AiScriptNamespaceError,
AiScriptRuntimeError,
AiScriptIndexOutOfRangeError
}
Expand All @@ -248,7 +260,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 & {
Expand All @@ -262,7 +274,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 & {
Expand Down Expand Up @@ -595,6 +607,8 @@ declare namespace values {
VArr,
VObj,
VFn,
VUserFn,
VNativeFn,
VReturn,
VBreak,
VContinue,
Expand Down Expand Up @@ -651,18 +665,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<Value>;
topCall: (fn: VFn, args: Value[]) => Promise<Value>;
registerAbortHandler: (handler: () => void) => void;
unregisterAbortHandler: (handler: () => void) => void;
}) => Value | Promise<Value> | void;
scope?: Scope;
};

// @public (undocumented)
Expand Down Expand Up @@ -694,6 +707,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)

```
11 changes: 11 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -46,6 +47,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.
*/
Expand Down
32 changes: 20 additions & 12 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Value> {
Expand All @@ -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<Value> {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -247,7 +242,20 @@ export class Interpreter {
}

@autobind
private async _eval(node: Ast.Node, scope: Scope): Promise<Value> {
private _eval(node: Ast.Node, scope: Scope): Promise<Value> {
return this.__eval(node, scope).catch(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;
}
});
}

@autobind
private async __eval(node: Ast.Node, scope: Scope): Promise<Value> {
if (this.stop) return NULL;
if (this.stepCount % IRQ_RATE === IRQ_AT) await new Promise(resolve => setTimeout(resolve, 5));
this.stepCount++;
Expand Down
22 changes: 14 additions & 8 deletions src/interpreter/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,26 @@ export type VObj = {
value: Map<string, Value>;
};

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<Value>;
topCall: (fn: VFn, args: Value[]) => Promise<Value>;
registerAbortHandler: (handler: () => void) => void;
unregisterAbortHandler: (handler: () => void) => void;
}) => Value | Promise<Value> | void;
scope?: Scope;
};

export type VReturn = {
Expand Down Expand Up @@ -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,
});
Expand Down
62 changes: 0 additions & 62 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> = 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));
Expand Down
Loading