diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 70e7f955..95be0bb1 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -218,9 +218,6 @@ type Bool = NodeBase & { value: boolean; }; -// @public (undocumented) -const BREAK: () => Value; - // @public (undocumented) type Break = NodeBase & { type: 'break'; @@ -233,9 +230,6 @@ type Call = NodeBase & { args: Expression[]; }; -// @public (undocumented) -const CONTINUE: () => Value; - // @public (undocumented) type Continue = NodeBase & { type: 'continue'; @@ -634,9 +628,6 @@ type Rem = NodeBase & { // @public (undocumented) function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set): string; -// @public (undocumented) -const RETURN: (v: VReturn["value"]) => Value; - // @public (undocumented) type Return = NodeBase & { type: 'return'; @@ -710,9 +701,6 @@ const TRUE: { // @public (undocumented) type TypeSource = NamedTypeSource | FnTypeSource; -// @public (undocumented) -const unWrapRet: (v: Value) => Value; - declare namespace utils { export { expectAny, @@ -746,7 +734,7 @@ function valToJs(val: Value): JsValue; function valToString(val: Value, simple?: boolean): string; // @public (undocumented) -type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr_2; +type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr_2; declare namespace values { export { @@ -760,9 +748,6 @@ declare namespace values { VUserFn, VFnParam, VNativeFn, - VReturn, - VBreak, - VContinue, VError, Attr_2 as Attr, Value, @@ -776,10 +761,6 @@ declare namespace values { ARR, FN, FN_NATIVE, - RETURN, - BREAK, - CONTINUE, - unWrapRet, ERROR } } @@ -797,18 +778,6 @@ type VBool = { value: boolean; }; -// @public (undocumented) -type VBreak = { - type: 'break'; - value: null; -}; - -// @public (undocumented) -type VContinue = { - type: 'continue'; - value: null; -}; - // @public (undocumented) type VError = { type: 'error'; @@ -857,12 +826,6 @@ type VObj = { value: Map; }; -// @public (undocumented) -type VReturn = { - type: 'return'; - value: Value; -}; - // @public (undocumented) type VStr = { type: 'str'; @@ -882,7 +845,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:47:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:48: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/control.ts b/src/interpreter/control.ts new file mode 100644 index 00000000..e748344e --- /dev/null +++ b/src/interpreter/control.ts @@ -0,0 +1,82 @@ +import { AiScriptRuntimeError } from '../error.js'; +import type { Reference } from './reference.js'; +import type { Value } from './value.js'; + +export type CReturn = { + type: 'return'; + value: Value; +}; + +export type CBreak = { + type: 'break'; + value: null; +}; + +export type CContinue = { + type: 'continue'; + value: null; +}; + +export type Control = CReturn | CBreak | CContinue; + +// Return文で値が返されたことを示すためのラッパー +export const RETURN = (v: CReturn['value']): CReturn => ({ + type: 'return' as const, + value: v, +}); + +export const BREAK = (): CBreak => ({ + type: 'break' as const, + value: null, +}); + +export const CONTINUE = (): CContinue => ({ + type: 'continue' as const, + value: null, +}); + +export function unWrapRet(v: Value | Control): Value { + switch (v.type) { + case 'return': + return v.value; + default: { + assertValue(v); + return v; + } + } +} + +export function assertValue(v: Value | Control): asserts v is Value { + switch (v.type) { + case 'return': + throw new AiScriptRuntimeError('Invalid return'); + case 'break': + throw new AiScriptRuntimeError('Invalid break'); + case 'continue': + throw new AiScriptRuntimeError('Invalid continue'); + default: + v satisfies Value; + } +} + +export function isControl(v: Value | Control | Reference): v is Control { + switch (v.type) { + case 'null': + case 'bool': + case 'num': + case 'str': + case 'arr': + case 'obj': + case 'fn': + case 'error': + case 'reference': + return false; + case 'return': + case 'break': + case 'continue': + return true; + } + // exhaustive check + v satisfies never; + throw new TypeError('expected value or control'); +} diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 86ee989e..c314fd99 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -7,13 +7,14 @@ import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexO import * as Ast from '../node.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; +import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control } from './control.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 { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; import { Reference } from './reference.js'; import type { JsValue } from './util.js'; -import type { Value, VFn } from './value.js'; +import type { Value, VFn, VUserFn } from './value.js'; export type LogObject = { scope?: string; @@ -107,6 +108,7 @@ export class Interpreter { try { await this.collectNs(script); const result = await this._run(script, this.scope, []); + assertValue(result); this.log('end', { val: result }); } catch (e) { this.handleError(e); @@ -234,6 +236,7 @@ export class Interpreter { } const value = await this._eval(node.expr, nsScope, []); + assertValue(value); if ( node.expr.type === 'fn' && isFunction(value) @@ -290,12 +293,27 @@ export class Interpreter { } @autobind - private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + 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, callStack: readonly CallInfo[]): Promise { + private async _evalBinaryOperation(op: string, leftExpr: Ast.Expression, rightExpr: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + const callee = scope.get(op); + assertFunction(callee); + const left = await this._eval(leftExpr, scope, callStack); + if (isControl(left)) { + return left; + } + const right = await this._eval(rightExpr, scope, callStack); + if (isControl(right)) { + return right; + } + return this._fn(callee, [left, right], callStack); + } + + @autobind + 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 { @@ -316,7 +334,7 @@ export class Interpreter { } @autobind - private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { + private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { if (this.stop) return NULL; if (this.pausing) await this.pausing.promise; // irqRateが小数の場合は不等間隔になる @@ -331,19 +349,35 @@ export class Interpreter { switch (node.type) { case 'call': { const callee = await this._eval(node.target, scope, callStack); + if (isControl(callee)) { + return callee; + } assertFunction(callee); - const args = await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack))); + const args = []; + for (const expr of node.args) { + const arg = await this._eval(expr, scope, callStack); + if (isControl(arg)) { + return arg; + } + args.push(arg); + } return this._fn(callee, args, callStack, node.loc.start); } case 'if': { const cond = await this._eval(node.cond, scope, callStack); + if (isControl(cond)) { + return cond; + } assertBoolean(cond); if (cond.value) { return this._evalClause(node.then, scope, callStack); } for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); + if (isControl(cond)) { + return cond; + } assertBoolean(cond); if (cond.value) { return this._evalClause(elseif.then, scope, callStack); @@ -357,8 +391,14 @@ export class Interpreter { case 'match': { const about = await this._eval(node.about, scope, callStack); + if (isControl(about)) { + return about; + } for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); + if (isControl(q)) { + return q; + } if (eq(about, q)) { return await this._evalClause(qa.a, scope, callStack); } @@ -385,6 +425,9 @@ export class Interpreter { case 'for': { if (node.times) { const times = await this._eval(node.times, scope, callStack); + if (isControl(times)) { + return times; + } assertNumber(times); for (let i = 0; i < times.value; i++) { const v = await this._evalClause(node.for, scope, callStack); @@ -396,7 +439,13 @@ export class Interpreter { } } else { const from = await this._eval(node.from!, scope, callStack); + if (isControl(from)) { + return from; + } const to = await this._eval(node.to!, scope, callStack); + if (isControl(to)) { + return to; + } assertNumber(from); assertNumber(to); for (let i = from.value; i < from.value + to.value; i++) { @@ -418,6 +467,9 @@ export class Interpreter { case 'each': { const items = await this._eval(node.items, scope, callStack); + if (isControl(items)) { + return items; + } assertArray(items); for (const item of items.value) { const eachScope = scope.createChildScope(); @@ -434,12 +486,17 @@ export class Interpreter { case 'def': { const value = await this._eval(node.expr, scope, callStack); + if (isControl(value)) { + return value; + } if (node.attr.length > 0) { const attrs: Value['attr'] = []; for (const nAttr of node.attr) { + const value = await this._eval(nAttr.value, scope, callStack); + assertValue(value); attrs.push({ name: nAttr.name, - value: await this._eval(nAttr.value, scope, callStack), + value, }); } value.attr = attrs; @@ -462,7 +519,13 @@ export class Interpreter { case 'assign': { const target = await this.getReference(node.dest, scope, callStack); + if (isControl(target)) { + return target; + } const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } target.set(v); @@ -471,7 +534,13 @@ export class Interpreter { case 'addAssign': { const target = await this.getReference(node.dest, scope, callStack); + if (isControl(target)) { + return target; + } const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertNumber(v); const targetValue = target.get(); assertNumber(targetValue); @@ -482,7 +551,13 @@ export class Interpreter { case 'subAssign': { const target = await this.getReference(node.dest, scope, callStack); + if (isControl(target)) { + return target; + } const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertNumber(v); const targetValue = target.get(); assertNumber(targetValue); @@ -499,20 +574,35 @@ export class Interpreter { case 'str': return STR(node.value); - case 'arr': return ARR(await Promise.all( - node.value.map(item => this._eval(item, scope, callStack)) - )); + case 'arr': { + const value = []; + for (const item of node.value) { + const valueItem = await this._eval(item, scope, callStack); + if (isControl(valueItem)) { + return valueItem; + } + value.push(valueItem); + } + return ARR(value); + } case 'obj': { const obj = new Map(); - for (const [key, value] of node.value) { - obj.set(key, await this._eval(value, scope, callStack)); + for (const [key, valueExpr] of node.value) { + const value = await this._eval(valueExpr, scope, callStack); + if (isControl(value)) { + return value; + } + obj.set(key, value); } return OBJ(obj); } case 'prop': { const target = await this._eval(node.target, scope, callStack); + if (isControl(target)) { + return target; + } if (isObject(target)) { if (target.value.has(node.name)) { return target.value.get(node.name)!; @@ -526,7 +616,13 @@ export class Interpreter { case 'index': { const target = await this._eval(node.target, scope, callStack); + if (isControl(target)) { + return target; + } const i = await this._eval(node.index, scope, callStack); + if (isControl(i)) { + return i; + } if (isArray(target)) { assertNumber(i); const item = target.value[i.value]; @@ -548,22 +644,33 @@ export class Interpreter { case 'not': { const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertBoolean(v); return BOOL(!v.value); } case 'fn': { + const params = await Promise.all(node.params.map(async (param) => { + return { + dest: param.dest, + default: + param.default ? await this._eval(param.default, scope, callStack) : + param.optional ? NULL : + undefined, + // type: (TODO) + }; + })); + const control = params + .map((param) => param.default) + .filter((value) => value != null) + .find(isControl); + if (control != null) { + return control; + } return FN( - await Promise.all(node.params.map(async (param) => { - return { - dest: param.dest, - default: - param.default ? await this._eval(param.default, scope, callStack) : - param.optional ? NULL : - undefined, - // type: (TODO) - }; - })), + params as VUserFn['params'], node.children, scope, ); @@ -584,6 +691,9 @@ export class Interpreter { str += x; } else { const v = await this._eval(x, scope, callStack); + if (isControl(v)) { + return v; + } str += reprValue(v); } } @@ -592,6 +702,9 @@ export class Interpreter { case 'return': { const val = await this._eval(node.expr, scope, callStack); + if (isControl(val)) { + return val; + } this.log('block:return', { scope: scope.name, val: val }); return RETURN(val); } @@ -615,109 +728,67 @@ export class Interpreter { } case 'pow': { - const callee = scope.get('Core:pow'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:pow', node.left, node.right, scope, callStack); } case 'mul': { - const callee = scope.get('Core:mul'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:mul', node.left, node.right, scope, callStack); } case 'div': { - const callee = scope.get('Core:div'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:div', node.left, node.right, scope, callStack); } case 'rem': { - const callee = scope.get('Core:mod'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:mod', node.left, node.right, scope, callStack); } case 'add': { - const callee = scope.get('Core:add'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:add', node.left, node.right, scope, callStack); } case 'sub': { - const callee = scope.get('Core:sub'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:sub', node.left, node.right, scope, callStack); } case 'lt': { - const callee = scope.get('Core:lt'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:lt', node.left, node.right, scope, callStack); } case 'lteq': { - const callee = scope.get('Core:lteq'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:lteq', node.left, node.right, scope, callStack); } case 'gt': { - const callee = scope.get('Core:gt'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:gt', node.left, node.right, scope, callStack); } case 'gteq': { - const callee = scope.get('Core:gteq'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:gteq', node.left, node.right, scope, callStack); } case 'eq': { - const callee = scope.get('Core:eq'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:eq', node.left, node.right, scope, callStack); } case 'neq': { - const callee = scope.get('Core:neq'); - assertFunction(callee); - 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); + return this._evalBinaryOperation('Core:neq', node.left, node.right, scope, callStack); } case 'and': { const leftValue = await this._eval(node.left, scope, callStack); + if (isControl(leftValue)) { + return leftValue; + } assertBoolean(leftValue); if (!leftValue.value) { return leftValue; } else { const rightValue = await this._eval(node.right, scope, callStack); + if (isControl(rightValue)) { + return rightValue; + } assertBoolean(rightValue); return rightValue; } @@ -725,12 +796,18 @@ export class Interpreter { case 'or': { const leftValue = await this._eval(node.left, scope, callStack); + if (isControl(leftValue)) { + return leftValue; + } assertBoolean(leftValue); if (leftValue.value) { return leftValue; } else { const rightValue = await this._eval(node.right, scope, callStack); + if (isControl(rightValue)) { + return rightValue; + } assertBoolean(rightValue); return rightValue; } @@ -743,10 +820,10 @@ export class Interpreter { } @autobind - private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise { + private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise { this.log('block:enter', { scope: scope.name }); - let v: Value = NULL; + let v: Value | Control = NULL; for (let i = 0; i < program.length; i++) { const node = program[i]!; @@ -854,14 +931,20 @@ export class Interpreter { } @autobind - private async getReference(dest: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + private async getReference(dest: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { switch (dest.type) { case 'identifier': { return Reference.variable(dest.name, scope); } case 'index': { const assignee = await this._eval(dest.target, scope, callStack); + if (isControl(assignee)) { + return assignee; + } const i = await this._eval(dest.index, scope, callStack); + if (isControl(i)) { + return i; + } if (isArray(assignee)) { assertNumber(i); return Reference.index(assignee, i.value); @@ -874,18 +957,33 @@ export class Interpreter { } case 'prop': { const assignee = await this._eval(dest.target, scope, callStack); + if (isControl(assignee)) { + return assignee; + } assertObject(assignee); return Reference.prop(assignee, dest.name); } case 'arr': { - const items = await Promise.all(dest.value.map((item) => this.getReference(item, scope, callStack))); + const items: Reference[] = []; + for (const item of dest.value) { + const ref = await this.getReference(item, scope, callStack); + if (isControl(ref)) { + return ref; + } + items.push(ref); + } return Reference.arr(items); } case 'obj': { - const entries = new Map(await Promise.all([...dest.value].map( - async ([key, item]) => [key, await this.getReference(item, scope, callStack)] as const - ))); + const entries = new Map(); + for (const [key, item] of dest.value.entries()) { + const ref = await this.getReference(item, scope, callStack); + if (isControl(ref)) { + return ref; + } + entries.set(key, ref); + } return Reference.obj(entries); } default: { diff --git a/src/interpreter/reference.ts b/src/interpreter/reference.ts index ce7f0e1e..1b6472a0 100644 --- a/src/interpreter/reference.ts +++ b/src/interpreter/reference.ts @@ -5,6 +5,8 @@ import type { VArr, VObj, Value } from './value.js'; import type { Scope } from './scope.js'; export interface Reference { + type: 'reference'; + get(): Value; set(value: Value): void; @@ -33,7 +35,11 @@ export const Reference = { }; class VariableReference implements Reference { - constructor(private name: string, private scope: Scope) {} + constructor(private name: string, private scope: Scope) { + this.type = 'reference'; + } + + type: 'reference'; get(): Value { return this.scope.get(this.name); @@ -45,7 +51,11 @@ class VariableReference implements Reference { } class IndexReference implements Reference { - constructor(private target: Value[], private index: number) {} + constructor(private target: Value[], private index: number) { + this.type = 'reference'; + } + + type: 'reference'; get(): Value { this.assertIndexInRange(); @@ -66,7 +76,11 @@ class IndexReference implements Reference { } class PropReference implements Reference { - constructor(private target: Map, private index: string) {} + constructor(private target: Map, private index: string) { + this.type = 'reference'; + } + + type: 'reference'; get(): Value { return this.target.get(this.index) ?? NULL; @@ -78,7 +92,11 @@ class PropReference implements Reference { } class ArrReference implements Reference { - constructor(private items: readonly Reference[]) {} + constructor(private items: readonly Reference[]) { + this.type = 'reference'; + } + + type: 'reference'; get(): Value { return ARR(this.items.map((item) => item.get())); @@ -93,7 +111,11 @@ class ArrReference implements Reference { } class ObjReference implements Reference { - constructor(private entries: ReadonlyMap) {} + constructor(private entries: ReadonlyMap) { + this.type = 'reference'; + } + + type: 'reference'; get(): Value { return OBJ(new Map([...this.entries].map(([key, item]) => [key, item.get()]))); diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 8fc5d4ab..0b41f71c 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -63,21 +63,6 @@ export type VNativeFn = VFnBase & { }) => Value | Promise | void; }; -export type VReturn = { - type: 'return'; - value: Value; -}; - -export type VBreak = { - type: 'break'; - value: null; -}; - -export type VContinue = { - type: 'continue'; - value: null; -}; - export type VError = { type: 'error'; value: string; @@ -91,7 +76,7 @@ export type Attr = { }[]; }; -export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr; +export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr; export const NULL = { type: 'null' as const, @@ -144,24 +129,6 @@ export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ 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; - export const ERROR = (name: string, info?: Value): Value => ({ type: 'error' as const, value: name, diff --git a/src/parser/index.ts b/src/parser/index.ts index 07092dbb..74f535fd 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,6 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; +import { validateJumpStatements } from './plugins/validate-jump-statements.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -21,6 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, + validateJumpStatements, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts new file mode 100644 index 00000000..d1a346e2 --- /dev/null +++ b/src/parser/plugins/validate-jump-statements.ts @@ -0,0 +1,49 @@ +import { visitNode } from '../visit.js'; +import { AiScriptSyntaxError } from '../../error.js'; + +import type * as Ast from '../../node.js'; + +function isInLoopScope(ancestors: Ast.Node[]): boolean { + for (let i = ancestors.length - 1; i >= 0; i--) { + switch (ancestors[i]!.type) { + case 'loop': + case 'for': + case 'each': + return true; + case 'fn': + return false; + } + } + return false; +} + +function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { + switch (node.type) { + case 'return': { + if (!ancestors.some(({ type }) => type === 'fn')) { + throw new AiScriptSyntaxError('return must be inside function', node.loc.start); + } + break; + } + case 'break': { + if (!isInLoopScope(ancestors)) { + throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); + } + break; + } + case 'continue': { + if (!isInLoopScope(ancestors)) { + throw new AiScriptSyntaxError('continue must be inside for / each / while / do-while / loop', node.loc.start); + } + break; + } + } + return node; +} + +export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { + for (const node of nodes) { + visitNode(node, validateNode); + } + return nodes; +} diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 84c30e34..52bbf48f 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -1,137 +1,142 @@ import type * as Ast from '../node.js'; -export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast.Node { - const result = fn(node); +export function visitNode(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node): Ast.Node { + return visitNodeInner(node, fn, []); +} + +function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node, ancestors: Ast.Node[]): Ast.Node { + const result = fn(node, ancestors); + ancestors.push(node); // nested nodes switch (result.type) { case 'def': { - result.expr = visitNode(result.expr, fn) as Ast.Definition['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr']; break; } case 'return': { - result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; break; } case 'each': { - result.items = visitNode(result.items, fn) as Ast.Each['items']; - result.for = visitNode(result.for, fn) as Ast.Each['for']; + result.items = visitNodeInner(result.items, fn, ancestors) as Ast.Each['items']; + result.for = visitNodeInner(result.for, fn, ancestors) as Ast.Each['for']; break; } case 'for': { if (result.from != null) { - result.from = visitNode(result.from, fn) as Ast.For['from']; + result.from = visitNodeInner(result.from, fn, ancestors) as Ast.For['from']; } if (result.to != null) { - result.to = visitNode(result.to, fn) as Ast.For['to']; + result.to = visitNodeInner(result.to, fn, ancestors) as Ast.For['to']; } if (result.times != null) { - result.times = visitNode(result.times, fn) as Ast.For['times']; + result.times = visitNodeInner(result.times, fn, ancestors) as Ast.For['times']; } - result.for = visitNode(result.for, fn) as Ast.For['for']; + result.for = visitNodeInner(result.for, fn, ancestors) as Ast.For['for']; break; } case 'loop': { for (let i = 0; i < result.statements.length; i++) { - result.statements[i] = visitNode(result.statements[i]!, fn) as Ast.Loop['statements'][number]; + result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Loop['statements'][number]; } break; } case 'addAssign': case 'subAssign': case 'assign': { - result.expr = visitNode(result.expr, fn) as Ast.Assign['expr']; - result.dest = visitNode(result.dest, fn) as Ast.Assign['dest']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Assign['expr']; + result.dest = visitNodeInner(result.dest, fn, ancestors) as Ast.Assign['dest']; break; } case 'not': { - result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; break; } case 'if': { - result.cond = visitNode(result.cond, fn) as Ast.If['cond']; - result.then = visitNode(result.then, fn) as Ast.If['then']; + result.cond = visitNodeInner(result.cond, fn, ancestors) as Ast.If['cond']; + result.then = visitNodeInner(result.then, fn, ancestors) as Ast.If['then']; for (const prop of result.elseif) { - prop.cond = visitNode(prop.cond, fn) as Ast.If['elseif'][number]['cond']; - prop.then = visitNode(prop.then, fn) as Ast.If['elseif'][number]['then']; + prop.cond = visitNodeInner(prop.cond, fn, ancestors) as Ast.If['elseif'][number]['cond']; + prop.then = visitNodeInner(prop.then, fn, ancestors) as Ast.If['elseif'][number]['then']; } if (result.else != null) { - result.else = visitNode(result.else, fn) as Ast.If['else']; + result.else = visitNodeInner(result.else, fn, ancestors) as Ast.If['else']; } break; } case 'fn': { for (const param of result.params) { if (param.default) { - param.default = visitNode(param.default!, fn) as Ast.Fn['params'][number]['default']; + param.default = visitNodeInner(param.default!, fn, ancestors) as Ast.Fn['params'][number]['default']; } } for (let i = 0; i < result.children.length; i++) { - result.children[i] = visitNode(result.children[i]!, fn) as Ast.Fn['children'][number]; + result.children[i] = visitNodeInner(result.children[i]!, fn, ancestors) as Ast.Fn['children'][number]; } break; } case 'match': { - result.about = visitNode(result.about, fn) as Ast.Match['about']; + result.about = visitNodeInner(result.about, fn, ancestors) as Ast.Match['about']; for (const prop of result.qs) { - prop.q = visitNode(prop.q, fn) as Ast.Match['qs'][number]['q']; - prop.a = visitNode(prop.a, fn) as Ast.Match['qs'][number]['a']; + prop.q = visitNodeInner(prop.q, fn, ancestors) as Ast.Match['qs'][number]['q']; + prop.a = visitNodeInner(prop.a, fn, ancestors) as Ast.Match['qs'][number]['a']; } if (result.default != null) { - result.default = visitNode(result.default, fn) as Ast.Match['default']; + result.default = visitNodeInner(result.default, fn, ancestors) as Ast.Match['default']; } break; } case 'block': { for (let i = 0; i < result.statements.length; i++) { - result.statements[i] = visitNode(result.statements[i]!, fn) as Ast.Block['statements'][number]; + result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Block['statements'][number]; } break; } case 'exists': { - result.identifier = visitNode(result.identifier,fn) as Ast.Exists['identifier']; + result.identifier = visitNodeInner(result.identifier, fn, ancestors) as Ast.Exists['identifier']; break; } case 'tmpl': { for (let i = 0; i < result.tmpl.length; i++) { const item = result.tmpl[i]!; if (typeof item !== 'string') { - result.tmpl[i] = visitNode(item, fn) as Ast.Tmpl['tmpl'][number]; + result.tmpl[i] = visitNodeInner(item, fn, ancestors) as Ast.Tmpl['tmpl'][number]; } } break; } case 'obj': { for (const item of result.value) { - result.value.set(item[0], visitNode(item[1], fn) as Ast.Expression); + result.value.set(item[0], visitNodeInner(item[1], fn, ancestors) as Ast.Expression); } break; } case 'arr': { for (let i = 0; i < result.value.length; i++) { - result.value[i] = visitNode(result.value[i]!, fn) as Ast.Arr['value'][number]; + result.value[i] = visitNodeInner(result.value[i]!, fn, ancestors) as Ast.Arr['value'][number]; } break; } case 'call': { - result.target = visitNode(result.target, fn) as Ast.Call['target']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Call['target']; for (let i = 0; i < result.args.length; i++) { - result.args[i] = visitNode(result.args[i]!, fn) as Ast.Call['args'][number]; + result.args[i] = visitNodeInner(result.args[i]!, fn, ancestors) as Ast.Call['args'][number]; } break; } case 'index': { - result.target = visitNode(result.target, fn) as Ast.Index['target']; - result.index = visitNode(result.index, fn) as Ast.Index['index']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Index['target']; + result.index = visitNodeInner(result.index, fn, ancestors) as Ast.Index['index']; break; } case 'prop': { - result.target = visitNode(result.target, fn) as Ast.Prop['target']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Prop['target']; break; } case 'ns': { for (let i = 0; i < result.members.length; i++) { - result.members[i] = visitNode(result.members[i]!, fn) as (typeof result.members)[number]; + result.members[i] = visitNodeInner(result.members[i]!, fn, ancestors) as (typeof result.members)[number]; } break; } @@ -150,7 +155,7 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast case 'neq': case 'and': case 'or': { - result.left = visitNode(result.left, fn) as ( + result.left = visitNodeInner(result.left, fn, ancestors) as ( Ast.Pow | Ast.Mul | Ast.Div | @@ -166,7 +171,7 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast Ast.And | Ast.Or )['left']; - result.right = visitNode(result.right, fn) as ( + result.right = visitNodeInner(result.right, fn, ancestors) as ( Ast.Pow | Ast.Mul | Ast.Div | @@ -186,5 +191,6 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast } } + ancestors.pop(); return result; } diff --git a/test/jump-statements.ts b/test/jump-statements.ts new file mode 100644 index 00000000..80470d9a --- /dev/null +++ b/test/jump-statements.ts @@ -0,0 +1,698 @@ +import * as assert from 'assert'; +import { describe, test } from 'vitest'; +import { utils } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptRuntimeError } from '../src/error'; +import { exe, getMeta, eq } from './testutils'; + +describe('return', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + @f() { + return 1 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('return 1')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + @f() { + let a = eval { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: eval { return 1 }')); + }); + + describe('in if', () => { + test.concurrent('cond', async () => { + const res = await exe(` + @f() { + let a = if eval { return true } {} + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: if eval { return true } {}')); + }); + + test.concurrent('then', async () => { + const res = await exe(` + @f() { + let a = if true { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if true { return 1 }')); + }); + + test.concurrent('elif cond', async () => { + const res = await exe(` + @f() { + let a = if false {} elif eval { return true } {} + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); + }); + + test.concurrent('elif then', async () => { + const res = await exe(` + @f() { + let a = if false { + } elif true { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if false {} elif true eval { return true }')); + }); + + test.concurrent('else', async () => { + const res = await exe(` + @f() { + let a = if false { + } else { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if false {} else eval { return true }')); + }); + }); + + describe('in match', () => { + test.concurrent('about', async () => { + const res = await exe(` + @f() { + let a = match eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match eval { return 1 } {}')); + }); + + test.concurrent('case q', async () => { + const res = await exe(` + @f() { + let a = match 0 { + case eval { return 0 } => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) + }); + + test.concurrent('case a', async () => { + const res = await exe(` + @f() { + let a = match 0 { + case 0 => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match 0 { case 0 => { return 1 } }')) + }); + + test.concurrent('default', async () => { + const res = await exe(` + @f() { + let a = match 0 { + default => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) + }); + }); + + describe('in binary operation', () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } + 2 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: eval { return 1 } + 2')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + 1 + eval { return 2 } + } + <: f() + `); + eq(res, NUM(2)); + assert.rejects(() => exe('<: 1 + eval { return 2 }')); + }); + }); + + describe('in call', () => { + test.concurrent('callee', async () => { + const res = await exe(` + @f() { + eval { return print }('Hello, world!') + } + f()('Hi') + `); + eq(res, STR('Hi')); + assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); + }); + + test.concurrent('arg', async () => { + const res = await exe(` + @f() { + print(eval { return 'Hello, world!' }) + } + <: f() + `); + eq(res, STR('Hello, world!')); + assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) + }); + }); + + describe('in for', () => { + test.concurrent('times', async () => { + const res = await exe(` + @f() { + for eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for eval { return 1 } {}')); + }); + + test.concurrent('from', async () => { + const res = await exe(` + @f() { + for let i = eval { return 1 }, 2 {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); + }); + + test.concurrent('to', async () => { + const res = await exe(` + @f() { + for let i = 0, eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); + }); + + test.concurrent('for', async () => { + const res = await exe(` + @f() { + for 1 { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for 1 { return 1 }')); + }) + }); + + describe('in each', () => { + test.concurrent('items', async () => { + const res = await exe(` + @f() { + each let v, [eval { return 1 }] {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); + }); + + test.concurrent('for', async () => { + const res = await exe(` + @f() { + each let v, [0] { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('each let v, [0] { return 1 }')); + }); + }); + + describe('in assign', () => { + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = null + a = eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = null; a = eval { return 1 }')); + }); + + test.concurrent('index target', async () => { + const res = await exe(` + @f() { + let a = [null] + eval { return a }[0] = 1 + } + <: f() + `); + eq(res, ARR([NULL])); + assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); + }); + + test.concurrent('index', async () => { + const res = await exe(` + @f() { + let a = [null] + a[eval { return 0 }] = 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); + }); + + test.concurrent('prop target', async () => { + const res = await exe(` + @f() { + let o = {} + eval { return o }.p = 1 + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); + }); + + test.concurrent('arr', async () => { + const res = await exe(` + @f() { + let o = {} + [eval { return o }.p] = [1] + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); + }); + + test.concurrent('obj', async () => { + const res = await exe(` + @f() { + let o = {} + { a: eval { return o }.p } = { a: 1 } + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); + }); + }); + + describe('in add assign', () => { + test.concurrent('dest', async () => { + const res = await exe(` + @f() { + let a = [0] + a[eval { return 0 }] += 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = 0 + a += eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); + }); + }); + + describe('in sub assign', () => { + test.concurrent('dest', async () => { + const res = await exe(` + @f() { + let a = [0] + a[eval { return 0 }] -= 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = 0 + a -= eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); + }); + }); + + test.concurrent('in array', async () => { + const res = await exe(` + @f() { + let a = [eval { return 1 }] + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: [eval { return 1 }]')); + }); + + test.concurrent('in object', async () => { + const res = await exe(` + @f() { + let o = { + p: eval { return 1 } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: { p: eval { return 1 } }')); + }); + + test.concurrent('in prop', async () => { + const res = await exe(` + @f() { + let p = { + p: eval { return 1 } + }.p + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); + }); + + describe('in index', () => { + test.concurrent('target', async () => { + const res = await exe(` + @f() { + let v = [eval { return 1 }][0] + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: [eval { return 1 }][0]')); + }); + + test.concurrent('index', async () => { + const res = await exe(` + @f() { + let v = [1][eval { return 0 }] + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: [0][eval { return 1 }]')); + }); + }); + + test.concurrent('in not', async () => { + const res = await exe(` + @f() { + let b = !eval { return true } + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: !eval { return true }')); + }); + + test.concurrent('in function default param', async () => { + const res = await exe(` + @f() { + let g = @(x = eval { return 1 }) {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); + }); + + test.concurrent('in template', async () => { + const res = await exe(` + @f() { + let s = \`{eval { return 1 }}\` + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: `{eval {return 1}}`')); + }); + + test.concurrent('in return', async () => { + const res = await exe(` + @f() { + return eval { return 1 } + 2 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('return eval { return 1 } + 2')); + }); + + describe('in and', async () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } && false + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('eval { return 1 } && false')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + true && eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('true && eval { return 1 }')); + }); + }); + + describe('in or', async () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } || false + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('eval { return 1 } || false')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + false || eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('false || eval { return 1 }')); + }); + }); +}); + +describe('break', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + break + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('break')); + assert.rejects(() => exe('@() { break }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { break }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); + + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + break; + } + } + `)); + }); +}); + +describe('continue', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + continue + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('continue')); + assert.rejects(() => exe('@() { continue }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { continue }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); + + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + continue; + } + } + `)); + }); +}); diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md new file mode 100644 index 00000000..eb1c1233 --- /dev/null +++ b/unreleased/jump-statements.md @@ -0,0 +1,6 @@ +- **Breaking Change** return文、break文、continue文の挙動が変更されました。 + - return文は関数スコープ内でないと文法エラーになります。 + - break文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 + - return文は常に関数から脱出します。 + - break文は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 + - continue文は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。