diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 96b96d67..ce52ea50 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -171,9 +171,6 @@ type Bool_2 = NodeBase_2 & ChainProp & { value: boolean; }; -// @public (undocumented) -const BREAK: () => Value; - // @public (undocumented) type Break = NodeBase & { type: 'break'; @@ -184,6 +181,13 @@ type Break_2 = NodeBase_2 & { type: 'break'; }; +// @public (undocumented) +class BreakError extends AiScriptError { + constructor(nodeType: 'break' | 'continue', message: string, info?: any); + // (undocumented) + nodeType: 'break' | 'continue'; +} + // @public (undocumented) function CALL(target: Call_2['target'], args: Call_2['args'], loc?: { start: number; @@ -213,9 +217,6 @@ type CallChain = NodeBase_2 & { // @public (undocumented) type ChainMember = CallChain | IndexChain | PropChain; -// @public (undocumented) -const CONTINUE: () => Value; - // @public (undocumented) type Continue = NodeBase & { type: 'continue'; @@ -324,7 +325,9 @@ declare namespace errors { SyntaxError_2 as SyntaxError, TypeError_2 as TypeError, RuntimeError, - IndexOutOfRangeError + IndexOutOfRangeError, + ReturnError, + BreakError } } export { errors } @@ -733,9 +736,6 @@ type PropChain = NodeBase_2 & { name: string; }; -// @public (undocumented) -const RETURN: (v: VReturn['value']) => Value; - // @public (undocumented) type Return = NodeBase & { type: 'return'; @@ -748,6 +748,15 @@ type Return_2 = NodeBase_2 & { expr: Expression_2; }; +// @public (undocumented) +class ReturnError extends AiScriptError { + constructor(val: Value, message: string, info?: any); + // (undocumented) + nodeType: string; + // (undocumented) + val: Value; +} + // @public (undocumented) class RuntimeError extends AiScriptError { constructor(message: string, info?: any); @@ -841,9 +850,6 @@ type TypeSource = NamedTypeSource | FnTypeSource; // @public (undocumented) type TypeSource_2 = NamedTypeSource_2 | FnTypeSource_2; -// @public (undocumented) -const unWrapRet: (v: Value) => Value; - declare namespace utils { export { expectAny, @@ -875,7 +881,7 @@ function valToJs(val: Value): any; function valToString(val: Value, simple?: boolean): string; // @public (undocumented) -type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue) & Attr_2; +type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn) & Attr_2; declare namespace values { export { @@ -886,9 +892,6 @@ declare namespace values { VArr, VObj, VFn, - VReturn, - VBreak, - VContinue, Attr_2 as Attr, Value, NULL, @@ -900,11 +903,7 @@ declare namespace values { OBJ, ARR, FN, - FN_NATIVE, - RETURN, - BREAK, - CONTINUE, - unWrapRet + FN_NATIVE } } export { values } @@ -921,18 +920,6 @@ type VBool = { value: boolean; }; -// @public (undocumented) -type VBreak = { - type: 'break'; - value: null; -}; - -// @public (undocumented) -type VContinue = { - type: 'continue'; - value: null; -}; - // @public (undocumented) type VFn = { type: 'fn'; @@ -963,12 +950,6 @@ type VObj = { value: Map; }; -// @public (undocumented) -type VReturn = { - type: 'return'; - value: Value; -}; - // @public (undocumented) type VStr = { type: 'str'; diff --git a/src/error.ts b/src/error.ts index 73340ad6..327028b1 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,3 +1,5 @@ +import type { Value } from './interpreter/value'; + export abstract class AiScriptError extends Error { public info?: any; @@ -36,3 +38,27 @@ export class IndexOutOfRangeError extends RuntimeError { super(message, info); } } + +// return文でスコープ抜け出しを行う用 +// 関数定義ブロックがcatchしなかったらそのままエラーとして扱う +export class ReturnError extends AiScriptError { + public nodeType = 'return'; + constructor( + public val: Value, + message: string, + info?: any, + ) { + super(message, info); + } +} +// break/continue文でスコープ抜け出しを行う用 +// loop/for/eachがcatchしなかったらそのままエラーとして扱う +export class BreakError extends AiScriptError { + constructor( + public nodeType: 'break'|'continue', + message: string, + info?: any, + ) { + super(message, info); + } +} diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 974ce893..2fd6e2a6 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -3,11 +3,11 @@ */ import autobind from 'autobind-decorator'; -import { IndexOutOfRangeError, RuntimeError } from '../error'; +import { IndexOutOfRangeError, RuntimeError, ReturnError, BreakError } from '../error'; import { Scope } from './scope'; import { std } from './lib/std'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, isString, expectAny, isNumber } from './util'; -import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE } from './value'; +import { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN } from './value'; import { PRIMITIVE_PROPS } from './primitive-props'; import type { Value, VFn } from './value'; import type * as Ast from '../node'; @@ -175,7 +175,17 @@ export class Interpreter { _args.set(fn.args![i]!, args[i]!); } const fnScope = fn.scope!.createChildScope(_args); - return unWrapRet(await this._run(fn.statements!, fnScope)); + try { + return await this._run(fn.statements!, fnScope); + } catch (e) { + if (e.nodeType === 'return') { + expectAny(e.val); + this.log('block:return caught by function', { scope: fnScope.name, val: e.val }); + return e.val; + } else { + throw e; + } + } } } @@ -237,11 +247,18 @@ export class Interpreter { case 'loop': { // eslint-disable-next-line no-constant-condition while (true) { - const v = await this._run(node.statements, scope.createChildScope()); - if (v.type === 'break') { - break; - } else if (v.type === 'return') { - return v; + try { + await this._run(node.statements, scope.createChildScope()); + } catch (e) { + if (e.nodeType === 'break') { + this.log('block:break caught by loop', { scope: scope.name }); + break; + } else if (e.nodeType === 'continue') { + this.log('block:continue caught by loop', { scope: scope.name }); + continue; + } else { + throw e; + } } } return NULL; @@ -252,11 +269,18 @@ export class Interpreter { const times = await this._eval(node.times, scope); assertNumber(times); for (let i = 0; i < times.value; i++) { - const v = await this._eval(node.for, scope); - if (v.type === 'break') { - break; - } else if (v.type === 'return') { - return v; + try { + await this._eval(node.for, scope); + } catch (e) { + if (e.nodeType === 'break') { + this.log('block:break caught by for', { scope: scope.name }); + break; + } else if (e.nodeType === 'continue') { + this.log('block:continue caught by for', { scope: scope.name }); + continue; + } else { + throw e; + } } } } else { @@ -265,13 +289,20 @@ export class Interpreter { assertNumber(from); assertNumber(to); for (let i = from.value; i < from.value + to.value; i++) { - const v = await this._eval(node.for, scope.createChildScope(new Map([ - [node.var!, NUM(i)], - ]))); - if (v.type === 'break') { - break; - } else if (v.type === 'return') { - return v; + try { + await this._eval(node.for, scope.createChildScope(new Map([ + [node.var!, NUM(i)], + ]))); + } catch (e) { + if (e.nodeType === 'break') { + this.log('block:break caught by for', { scope: scope.name }); + break; + } else if (e.nodeType === 'continue') { + this.log('block:continue caught by for', { scope: scope.name }); + continue; + } else { + throw e; + } } } } @@ -282,13 +313,20 @@ export class Interpreter { const items = await this._eval(node.items, scope); assertArray(items); for (const item of items.value) { - const v = await this._eval(node.for, scope.createChildScope(new Map([ - [node.var, item], - ]))); - if (v.type === 'break') { - break; - } else if (v.type === 'return') { - return v; + try { + await this._eval(node.for, scope.createChildScope(new Map([ + [node.var, item], + ]))); + } catch (e) { + if (e.nodeType === 'break') { + this.log('block:break caught by each', { scope: scope.name }); + break; + } else if (e.nodeType === 'continue') { + this.log('block:continue caught by each', { scope: scope.name }); + continue; + } else { + throw e; + } } } return NULL; @@ -449,17 +487,17 @@ export class Interpreter { case 'return': { const val = await this._eval(node.expr, scope); this.log('block:return', { scope: scope.name, val: val }); - return RETURN(val); + throw new ReturnError(val,'"return" statement outside function'); } case 'break': { this.log('block:break', { scope: scope.name }); - return BREAK(); + throw new BreakError('break','"break" statement outside loop'); } case 'continue': { this.log('block:continue', { scope: scope.name }); - return CONTINUE(); + throw new BreakError('continue','"continue" statement outside loop'); } case 'ns': { @@ -486,16 +524,6 @@ export class Interpreter { const node = program[i]!; v = await this._eval(node, scope); - if (v.type === 'return') { - this.log('block:return', { scope: scope.name, val: v.value }); - return v; - } else if (v.type === 'break') { - this.log('block:break', { scope: scope.name }); - return v; - } else if (v.type === 'continue') { - this.log('block:continue', { scope: scope.name }); - return v; - } } this.log('block:leave', { scope: scope.name, val: v }); diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 45addfcf..ba13ee48 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -129,7 +129,7 @@ export function valToJs(val: Value): any { return obj; } case 'str': return val.value; - default: throw new Error(`Unrecognized value type: ${val.type}`); + //default: throw new Error(`Unrecognized value type: ${val.type}`); } } diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 9de7d652..c0864025 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -42,21 +42,6 @@ export type VFn = { scope?: Scope; }; -export type VReturn = { - type: 'return'; - value: Value; -}; - -export type VBreak = { - type: 'break'; - value: null; -}; - -export type VContinue = { - type: 'continue'; - value: null; -}; - export type Attr = { attr?: { name: string; @@ -64,7 +49,7 @@ export type Attr = { }[]; }; -export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue) & Attr; +export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn ) & Attr; export const NULL = { type: 'null' as const, @@ -116,21 +101,3 @@ export const FN_NATIVE = (fn: VFn['native']): VFn => ({ type: 'fn' as const, native: fn, }); - -// Return文で値が返されたことを示すためのラッパー -export const RETURN = (v: VReturn['value']): Value => ({ - type: 'return' as const, - value: v, -}); - -export const BREAK = (): Value => ({ - type: 'break' as const, - value: null, -}); - -export const CONTINUE = (): Value => ({ - type: 'continue' as const, - value: null, -}); - -export const unWrapRet = (v: Value): Value => v.type === 'return' ? v.value : v; diff --git a/test/index.ts b/test/index.ts index d99c679e..c6f32168 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1561,6 +1561,70 @@ describe('for of', () => { }); }); +describe('特殊なスコープ脱出', () => { + test.concurrent('return in if', async () => { + const res = await exe(` + @func(cond){ + if cond return 1 else return 2 + } + <: [func(true), func(false)] + `); + eq(res, ARR([NUM(1), NUM(2)])); + }) + test.concurrent('break in eval', async () => { + const res = await exe(` + var cnt=0 + for let i=0,5 { + cnt+=1 + eval { + break + } + } + <: cnt + `); + eq(res, NUM(1)); + }) +}); + +describe('不正なスコープ脱出', () => { + test.concurrent('return in global loop', async () => { + try { + const res = await exe(` + loop { + return 0 + } + `); + assert.fail('error not thrown'); + } catch(e) { + if (e.nodeType==='return') { + assert.ok(true); + return; + } else { + assert.fail('unexpected error thrown'); + } + } + }); + + test.concurrent('break in global function', async () => { + try { + const res = await exe(` + @func() { + break + } + func() + `); + assert.fail('error not thrown'); + } catch(e) { + if (e.nodeType==='break') { + assert.ok(true); + return; + } else { + assert.fail('unexpected error thrown'); + } + } + }); +}); + describe('not', () => { test.concurrent('Basic', async () => { const res = await exe(`