diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index ed176ac7..c20a03e2 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -215,6 +215,7 @@ type Attribute = NodeBase & { // @public (undocumented) type Block = NodeBase & { type: 'block'; + label?: string; statements: (Statement | Expression)[]; }; @@ -230,6 +231,8 @@ type Bool = NodeBase & { // @public (undocumented) type Break = NodeBase & { type: 'break'; + label?: string; + expr?: Expression; }; // @public (undocumented) @@ -242,6 +245,7 @@ type Call = NodeBase & { // @public (undocumented) type Continue = NodeBase & { type: 'continue'; + label?: string; }; // @public (undocumented) @@ -264,6 +268,7 @@ type Div = NodeBase & { // @public (undocumented) type Each = NodeBase & { type: 'each'; + label?: string; var: Expression; items: Expression; for: Statement | Expression; @@ -347,6 +352,7 @@ type FnTypeSource = NodeBase & { // @public (undocumented) type For = NodeBase & { type: 'for'; + label?: string; var?: string; from?: Expression; to?: Expression; @@ -380,6 +386,7 @@ type Identifier = NodeBase & { // @public (undocumented) type If = NodeBase & { type: 'if'; + label?: string; cond: Expression; then: Statement | Expression; elseif: { @@ -479,6 +486,7 @@ type Loc = { // @public (undocumented) type Loop = NodeBase & { type: 'loop'; + label?: string; statements: (Statement | Expression)[]; }; @@ -499,6 +507,7 @@ type Lteq = NodeBase & { // @public (undocumented) type Match = NodeBase & { type: 'match'; + label?: string; about: Expression; qs: { q: Expression; diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index e748344e..91fe5aaf 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -1,4 +1,5 @@ import { AiScriptRuntimeError } from '../error.js'; +import { NULL } from './value.js'; import type { Reference } from './reference.js'; import type { Value } from './value.js'; @@ -9,11 +10,13 @@ export type CReturn = { export type CBreak = { type: 'break'; - value: null; + label?: string; + value?: Value; }; export type CContinue = { type: 'continue'; + label?: string; value: null; }; @@ -25,16 +28,28 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ value: v, }); -export const BREAK = (): CBreak => ({ +export const BREAK = (label?: string, value?: CBreak['value']): CBreak => ({ type: 'break' as const, - value: null, + label, + value: value, }); -export const CONTINUE = (): CContinue => ({ +export const CONTINUE = (label?: string): CContinue => ({ type: 'continue' as const, + label, value: null, }); +/** + * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 + */ +export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && v.label != null && v.label === label) { + return v.value ?? NULL; + } + return v; +} + export function unWrapRet(v: Value | Control): Value { switch (v.type) { case 'return': diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index acc12d21..8cdab2ae 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 { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control } from './control.js'; +import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control, unWrapLabeledBreak } from './control.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; import { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; @@ -371,7 +371,7 @@ export class Interpreter { } assertBoolean(cond); if (cond.value) { - return this._evalClause(node.then, scope, callStack); + return unWrapLabeledBreak(await this._evalClause(node.then, scope, callStack), node.label); } for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); @@ -380,11 +380,11 @@ export class Interpreter { } assertBoolean(cond); if (cond.value) { - return this._evalClause(elseif.then, scope, callStack); + return unWrapLabeledBreak(await this._evalClause(elseif.then, scope, callStack), node.label); } } if (node.else) { - return this._evalClause(node.else, scope, callStack); + return unWrapLabeledBreak(await this._evalClause(node.else, scope, callStack), node.label); } return NULL; } @@ -400,11 +400,11 @@ export class Interpreter { return q; } if (eq(about, q)) { - return await this._evalClause(qa.a, scope, callStack); + return unWrapLabeledBreak(await this._evalClause(qa.a, scope, callStack), node.label); } } if (node.default) { - return await this._evalClause(node.default, scope, callStack); + return unWrapLabeledBreak(await this._evalClause(node.default, scope, callStack), node.label); } return NULL; } @@ -414,7 +414,14 @@ export class Interpreter { while (true) { const v = await this._run(node.statements, scope.createChildScope(), callStack); if (v.type === 'break') { + if (v.label != null && v.label !== node.label) { + return v; + } break; + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -432,7 +439,14 @@ export class Interpreter { for (let i = 0; i < times.value; i++) { const v = await this._evalClause(node.for, scope, callStack); if (v.type === 'break') { + if (v.label != null && v.label !== node.label) { + return v; + } break; + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -456,7 +470,14 @@ export class Interpreter { }], ])), callStack); if (v.type === 'break') { + if (v.label != null && v.label !== node.label) { + return v; + } break; + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -476,7 +497,14 @@ export class Interpreter { this.define(eachScope, node.var, item, false); const v = await this._eval(node.for, eachScope, callStack); if (v.type === 'break') { + if (v.label != null && v.label !== node.label) { + return v; + } break; + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -695,7 +723,7 @@ export class Interpreter { } case 'block': { - return this._run(node.statements, scope.createChildScope(), callStack); + return unWrapLabeledBreak(await this._run(node.statements, scope.createChildScope(), callStack), node.label); } case 'exists': { @@ -728,13 +756,21 @@ export class Interpreter { } case 'break': { + let val: Value | undefined; + if (node.expr != null) { + const valueOrControl = await this._eval(node.expr, scope, callStack); + if (isControl(valueOrControl)) { + return valueOrControl; + } + val = valueOrControl; + } this.log('block:break', { scope: scope.name }); - return BREAK(); + return BREAK(node.label, val); } case 'continue': { this.log('block:continue', { scope: scope.name }); - return CONTINUE(); + return CONTINUE(node.label); } case 'ns': { diff --git a/src/node.ts b/src/node.ts index 2c73d695..1651e849 100644 --- a/src/node.ts +++ b/src/node.ts @@ -73,6 +73,7 @@ export type Return = NodeBase & { export type Each = NodeBase & { type: 'each'; // each文 + label?: string; // ラベル var: Expression; // イテレータ宣言 items: Expression; // 配列 for: Statement | Expression; // 本体処理 @@ -80,6 +81,7 @@ export type Each = NodeBase & { export type For = NodeBase & { type: 'for'; // for文 + label?: string; // ラベル var?: string; // イテレータ変数名 from?: Expression; // 開始値 to?: Expression; // 終値 @@ -89,15 +91,19 @@ export type For = NodeBase & { export type Loop = NodeBase & { type: 'loop'; // loop文 + label?: string; // ラベル statements: (Statement | Expression)[]; // 処理 }; export type Break = NodeBase & { type: 'break'; // break文 + label?: string; // ラベル + expr?: Expression; // 式 }; export type Continue = NodeBase & { type: 'continue'; // continue文 + label?: string; // ラベル }; export type AddAssign = NodeBase & { @@ -265,6 +271,7 @@ export type Or = NodeBase & { export type If = NodeBase & { type: 'if'; // if式 + label?: string; // ラベル cond: Expression; // 条件式 then: Statement | Expression; // then節 elseif: { @@ -289,6 +296,7 @@ export type Fn = NodeBase & { export type Match = NodeBase & { type: 'match'; // パターンマッチ + label?: string; // ラベル about: Expression; // 対象 qs: { q: Expression; // 条件 @@ -299,6 +307,7 @@ export type Match = NodeBase & { export type Block = NodeBase & { type: 'block'; // ブロックまたはeval式 + label?: string; // ラベル statements: (Statement | Expression)[]; // 処理 }; diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index d1a346e2..9828bca9 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -3,18 +3,31 @@ import { AiScriptSyntaxError } from '../../error.js'; import type * as Ast from '../../node.js'; -function isInLoopScope(ancestors: Ast.Node[]): boolean { +function getCorrespondingBlock(ancestors: Ast.Node[], label?: string): Ast.Each | Ast.For | Ast.Loop | Ast.If | Ast.Match | Ast.Block | undefined { for (let i = ancestors.length - 1; i >= 0; i--) { - switch (ancestors[i]!.type) { + const ancestor = ancestors[i]!; + switch (ancestor.type) { case 'loop': case 'for': - case 'each': - return true; + case 'each': { + if (label != null && label !== ancestor.label) { + continue; + } + return ancestor; + } + case 'if': + case 'match': + case 'block': { + if (label == null || label !== ancestor.label) { + continue; + } + return ancestor; + } case 'fn': - return false; + return; } } - return false; + return; } function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { @@ -26,14 +39,88 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (!isInLoopScope(ancestors)) { - throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); + const block = getCorrespondingBlock(ancestors, node.label); + if (block == null) { + if (node.label != null) { + throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); + } + throw new AiScriptSyntaxError('unlabeled break must be inside for / each / while / do-while / loop', node.loc.start); + } + + switch (block.type) { + case 'each': { + if (ancestors.includes(block.items)) { + throw new AiScriptSyntaxError('break corresponding to each is not allowed in the target', node.loc.start); + } + break; + } + case 'for': { + if (block.times != null && ancestors.includes(block.times)) { + throw new AiScriptSyntaxError('break corresponding to for is not allowed in the count', node.loc.start); + } else if (ancestors.some((ancestor) => ancestor === block.from || ancestor === block.to)) { + throw new AiScriptSyntaxError('break corresponding to for is not allowed in the range', node.loc.start); + } + break; + } + case 'if': { + if (ancestors.includes(block.cond) || block.elseif.some(({ cond }) => ancestors.includes(cond))) { + throw new AiScriptSyntaxError('break corresponding to if is not allowed in the condition', node.loc.start); + } + break; + } + case 'match':{ + if (ancestors.includes(block.about)) { + throw new AiScriptSyntaxError('break corresponding to match is not allowed in the target', node.loc.start); + } + if (block.qs.some(({ q }) => ancestors.includes(q))) { + throw new AiScriptSyntaxError('break corresponding to match is not allowed in the pattern', node.loc.start); + } + break; + } + } + + if (node.expr != null) { + switch (block.type) { + case 'if': + case 'match': + case 'block': + break; + default: + throw new AiScriptSyntaxError('break corresponding to statement cannot include value', node.loc.start); + } } break; } case 'continue': { - if (!isInLoopScope(ancestors)) { + const block = getCorrespondingBlock(ancestors, node.label); + if (block == null) { + if (node.label != null) { + throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); + } throw new AiScriptSyntaxError('continue must be inside for / each / while / do-while / loop', node.loc.start); + } else { + switch (block.type) { + case 'each': { + if (ancestors.includes(block.items)) { + throw new AiScriptSyntaxError('continue corresponding to each is not allowed in the target', node.loc.start); + } + break; + } + case 'for': { + if (block.times != null && ancestors.includes(block.times)) { + throw new AiScriptSyntaxError('continue corresponding to for is not allowed in the count', node.loc.start); + } else if (ancestors.some((ancestor) => ancestor === block.from || ancestor === block.to)) { + throw new AiScriptSyntaxError('continue corresponding to for is not allowed in the range', node.loc.start); + } + break; + } + case 'if': + throw new AiScriptSyntaxError('cannot use continue for if', node.loc.start); + case 'match': + throw new AiScriptSyntaxError('cannot use continue for match', node.loc.start); + case 'block': + throw new AiScriptSyntaxError('cannot use continue for eval', node.loc.start); + } } break; } diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 9ccfad78..c134232c 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -109,15 +109,39 @@ function validateNode(node: Ast.Node): Ast.Node { break; } case 'each': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } validateDest(node.var); break; } case 'for': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } if (node.var != null && reservedWord.includes(node.var)) { throwReservedWordError(node.var, node.loc); } break; } + case 'loop': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } + case 'break': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } + case 'continue': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } case 'fn': { validateTypeParams(node); for (const param of node.params) { diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 2802b06d..cb86984f 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -153,7 +153,7 @@ export class Scanner implements ITokenStream { this.stream.next(); return TOKEN(TokenKind.OpenSharpBracket, pos, { hasLeftSpacing }); } else { - throw new AiScriptSyntaxError('invalid character: "#"', pos); + return TOKEN(TokenKind.Sharp, pos, { hasLeftSpacing }); } } case '%': { diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 946c112c..049f8553 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -135,6 +135,25 @@ export function parseBlock(s: ITokenStream): (Ast.Statement | Ast.Expression)[] return steps; } +/** + * ```abnf + * Label = "#" IDENT + * ``` +*/ +export function parseLabel(s: ITokenStream): string { + s.expect(TokenKind.Sharp); + s.next(); + + if (s.getToken().hasLeftSpacing) { + throw new AiScriptSyntaxError('cannot use spaces in a label', s.getPos()); + } + s.expect(TokenKind.Identifier); + const label = s.getTokenValue(); + s.next(); + + return label; +} + /** * ```abnf * OptionalSeparator = [SEP] diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index ca86cd95..e249548b 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -2,7 +2,7 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js' import { NODE, unexpectedTokenError } from '../utils.js'; import { TokenStream } from '../streams/token-stream.js'; import { TokenKind } from '../token.js'; -import { parseBlock, parseOptionalSeparator, parseParams } from './common.js'; +import { parseBlock, parseLabel, parseOptionalSeparator, parseParams } from './common.js'; import { parseBlockOrStatement } from './statements.js'; import { parseType, parseTypeParams } from './types.js'; @@ -288,6 +288,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { s.next(); return expr; } + case TokenKind.Sharp: { + return parseExprWithLabel(s); + } } throw unexpectedTokenError(s.getTokenKind(), startPos); } @@ -343,6 +346,30 @@ function parseCall(s: ITokenStream, target: Ast.Expression): Ast.Call { }, startPos, s.getPos()); } +/** + * ```abnf + * ExprWithLabel = "#" IDENT ":" Expr + * ``` +*/ +function parseExprWithLabel(s: ITokenStream): Ast.If | Ast.Match | Ast.Block { + const label = parseLabel(s); + s.expect(TokenKind.Colon); + s.next(); + + const expr = parseExpr(s, false); + switch (expr.type) { + case 'if': + case 'match': + case 'block': { + expr.label = label; + return expr; + } + default: { + throw new AiScriptSyntaxError('cannot use label for expression other than eval / if / match', expr.loc.start); + } + } +} + /** * ```abnf * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 050c15d5..d8918adf 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -1,7 +1,7 @@ import { AiScriptSyntaxError } from '../../error.js'; import { CALL_NODE, NODE, unexpectedTokenError } from '../utils.js'; import { TokenKind } from '../token.js'; -import { parseBlock, parseDest, parseParams } from './common.js'; +import { parseBlock, parseDest, parseLabel, parseParams } from './common.js'; import { parseExpr } from './expressions.js'; import { parseType, parseTypeParams } from './types.js'; @@ -32,6 +32,9 @@ export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression case TokenKind.OpenSharpBracket: { return parseStatementWithAttr(s); } + case TokenKind.Sharp: { + return parseStatementWithLabel(s); + } case TokenKind.EachKeyword: { return parseEach(s); } @@ -48,12 +51,10 @@ export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression return parseWhile(s); } case TokenKind.BreakKeyword: { - s.next(); - return NODE('break', {}, startPos, s.getPos()); + return parseBreak(s); } case TokenKind.ContinueKeyword: { - s.next(); - return NODE('continue', {}, startPos, s.getPos()); + return parseContinue(s); } } const expr = parseExpr(s, false); @@ -207,6 +208,33 @@ function parseOut(s: ITokenStream): Ast.Call { return CALL_NODE('print', [expr], startPos, s.getPos()); } +/** + * ```abnf + * StatementWithLabel = "#" IDENT ":" Statement + * ``` +*/ +function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop | Ast.If | Ast.Match | Ast.Block { + const label = parseLabel(s); + s.expect(TokenKind.Colon); + s.next(); + + const statement = parseStatement(s); + switch (statement.type) { + case 'if': + case 'match': + case 'block': + case 'each': + case 'for': + case 'loop': { + statement.label = label; + return statement; + } + default: { + throw new AiScriptSyntaxError('cannot use label for statement other than eval / if / match / for / each / while / do-while / loop', statement.loc.start); + } + } +} + /** * ```abnf * Each = "each" "(" "let" Dest "," Expr ")" BlockOrStatement @@ -470,6 +498,51 @@ function parseWhile(s: ITokenStream): Ast.Loop { }, startPos, s.getPos()); } +/** + * ```abnf + * Break = "break" ["#" IDENT [Expr]] + * ``` +*/ +function parseBreak(s: ITokenStream): Ast.Break { + const startPos = s.getPos(); + + s.expect(TokenKind.BreakKeyword); + s.next(); + + let label: string | undefined; + let expr: Ast.Expression | undefined; + if (s.is(TokenKind.Sharp)) { + label = parseLabel(s); + + if (s.is(TokenKind.Colon)) { + throw new AiScriptSyntaxError('cannot omit label from break if expression is given', startPos); + } else if (!isStatementTerminator(s)) { + expr = parseExpr(s, false); + } + } + + return NODE('break', { label, expr }, startPos, s.getPos()); +} + +/** + * ```abnf + * Continue = "continue" ["#" IDENT] + * ``` +*/ +function parseContinue(s: ITokenStream): Ast.Continue { + const startPos = s.getPos(); + + s.expect(TokenKind.ContinueKeyword); + s.next(); + + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + label = parseLabel(s); + } + + return NODE('continue', { label }, startPos, s.getPos()); +} + /** * ```abnf * Assign = Expr ("=" / "+=" / "-=") Expr @@ -500,3 +573,19 @@ function tryParseAssign(s: ITokenStream, dest: Ast.Expression): Ast.Statement | } } } + +function isStatementTerminator(s: ITokenStream): boolean { + switch (s.getTokenKind()) { + case TokenKind.EOF: + case TokenKind.NewLine: + case TokenKind.WhileKeyword: + case TokenKind.ElifKeyword: + case TokenKind.ElseKeyword: + case TokenKind.Comma: + case TokenKind.SemiColon: + case TokenKind.CloseBrace: + return true; + default: + return false; + } +} diff --git a/src/parser/token.ts b/src/parser/token.ts index 94425cde..35b2d4c7 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -39,6 +39,8 @@ export enum TokenKind { Not, /** "!=" */ NotEq, + /** "#" */ + Sharp, /** "#[" */ OpenSharpBracket, /** "###" */ diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 80470d9a..905898dd 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -2,7 +2,7 @@ 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 { AiScriptRuntimeError, AiScriptSyntaxError } from '../src/error'; import { exe, getMeta, eq } from './testutils'; describe('return', () => { @@ -14,7 +14,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('return 1')); + await assert.rejects(() => exe('return 1')); }); test.concurrent('in eval', async () => { @@ -27,532 +27,549 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 }')); + await 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 }')); - }); + test.concurrent('cond', async () => { + const res = await exe(` + @f() { + let a = if eval { return true } {} + } + <: f() + `); + eq(res, BOOL(true)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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 }')); - }); - }); + test.concurrent('about', async () => { + const res = await exe(` + @f() { + let a = match eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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')); + await 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!')); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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])); + await 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)); + await 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())); + await 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())); + await 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())); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await 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)); + await assert.rejects(() => exe('return eval { return 1 } + 2')); + }); + + test.concurrent('in break', async () => { + const res = await exe(` + @f() { + #l: eval { + break #l eval { return 1 } + } + } + <: f() + `); + eq(res, NUM(1)); + await assert.rejects(() => exe(` + #l: eval { + break #l eval { return 1 } + } + `)); + }); + + describe('in and', async () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } && false + } + <: f() + `); + eq(res, NUM(1)); + await 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)); + await 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)); + await 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)); + await assert.rejects(() => exe('false || eval { return 1 }')); + }); + }); }); describe('break', () => { @@ -566,8 +583,8 @@ describe('break', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('break')); - assert.rejects(() => exe('@() { break }()')); + await assert.rejects(() => exe('break')); + await assert.rejects(() => exe('@() { break }()')); }); test.concurrent('in eval', async () => { @@ -582,7 +599,7 @@ describe('break', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: eval { break }')); + await assert.rejects(() => exe('<: eval { break }')); }); test.concurrent('in if', async () => { @@ -597,7 +614,7 @@ describe('break', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { break }')); + await assert.rejects(() => exe('<: if true { break }')); }); test.concurrent('in match', async () => { @@ -612,18 +629,575 @@ describe('break', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { break }')); - }); - - test.concurrent('in function', async () => { - assert.rejects(() => exe(` - for 1 { - @f() { - break; - } - } - `)); - }); + await assert.rejects(() => exe('<: if true { break }')); + }); + + test.concurrent('in function', async () => { + await assert.rejects(() => exe(` + for 1 { + @f() { + break; + } + } + `)); + }); + + test.concurrent('invalid label', async () => { + await assert.rejects(() => exe(` + for 1 { + break #l + } + `)); + }); + + test.concurrent('invalid value', async () => { + await assert.rejects(() => exe(` + #l: for 1 { + break #l 1 + } + `), new AiScriptSyntaxError('break corresponding to statement cannot include value', { line: 3, column: 4 })); + }); + + test.concurrent('invalid block', async () => { + await assert.rejects( + () => exe('#l: if true { continue #l }'), + new AiScriptSyntaxError('cannot use continue for if', { line: 1, column: 15 }), + ); + await assert.rejects( + () => exe('#l: match 0 { default => continue #l }'), + new AiScriptSyntaxError('cannot use continue for match', { line: 1, column: 26 }), + ); + await assert.rejects( + () => exe('#l: eval { continue #l }'), + new AiScriptSyntaxError('cannot use continue for eval', { line: 1, column: 12 }), + ); + }); + + test.concurrent('break corresponding to each is not allowed in the target', async () => { + await assert.rejects( + () => exe('#l: each let i, eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to each is not allowed in the target', { line: 1, column: 24 }), + ); + }); + + test.concurrent('break corresponding to for is not allowed in the count', async () => { + await assert.rejects( + () => exe('#l: for eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to for is not allowed in the count', { line: 1, column: 16 }), + ); + }); + + describe('break corresponding to for is not allowed in the range', () => { + test.concurrent('from', async () => { + await assert.rejects( + () => exe('#l: for let i = eval { break #l }, 0 {}'), + new AiScriptSyntaxError('break corresponding to for is not allowed in the range', { line: 1, column: 24 }), + ); + }); + + test.concurrent('to', async () => { + await assert.rejects( + () => exe('#l: for let i = 0, eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to for is not allowed in the range', { line: 1, column: 27 }), + ); + }); + }); + + describe('break corresponding to if is not allowed in the condition', () => { + test.concurrent('if', async () => { + await assert.rejects( + () => exe('#l: if eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to if is not allowed in the condition', { line: 1, column: 15 }), + ); + }); + + test.concurrent('elif', async () => { + await assert.rejects( + () => exe('#l: if false {} elif eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to if is not allowed in the condition', { line: 1, column: 29 }), + ); + }); + }); + + test.concurrent('break corresponding to match is not allowed in the target', async () => { + await assert.rejects( + () => exe('#l: match eval { break #l } {}'), + new AiScriptSyntaxError('break corresponding to match is not allowed in the target', { line: 1, column: 18 }), + ); + }); + + test.concurrent('break corresponding to match is not allowed in the pattern', async () => { + await assert.rejects( + () => exe('#l: match 0 { case eval { break #l } => 1 }'), + new AiScriptSyntaxError('break corresponding to match is not allowed in the pattern', { line: 1, column: 27 }), + ); + }); + + describe('labeled each', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled for', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled loop', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: loop { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: loop { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: loop { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: loop { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: loop { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled do-while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: do { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: do { + for 1 { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: do { + loop { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: do { + do { + x = 1 + break #l + } while false + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: do { + while true { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: while true { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: while true { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: while true { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: while true { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: while true { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled if', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: if true { + break #l + 2 + } + `); + eq(res, NULL); + }); + + test.concurrent('break with value', async () => { + const res = await exe(` + <: #l: if true { + break #l 1 + 2 + } + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled match', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: match 0 { + default => { + break #l + 2 + } + } + `); + eq(res, NULL); + }); + + test.concurrent('break with value', async () => { + const res = await exe(` + <: #l: match 0 { + default => { + break #l 1 + 2 + } + } + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled eval', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: eval { + break #l + 2 + } + `); + eq(res, NULL); + }); + + test.concurrent('break with value', async () => { + const res = await exe(` + <: #l: eval { + break #l 1 + 2 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + <: #l: eval { + while true { + if true break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + }); }); describe('continue', () => { @@ -637,8 +1211,8 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('continue')); - assert.rejects(() => exe('@() { continue }()')); + await assert.rejects(() => exe('continue')); + await assert.rejects(() => exe('@() { continue }()')); }); test.concurrent('in eval', async () => { @@ -653,7 +1227,7 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: eval { continue }')); + await assert.rejects(() => exe('<: eval { continue }')); }); test.concurrent('in if', async () => { @@ -668,7 +1242,7 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { continue }')); + await assert.rejects(() => exe('<: if true { continue }')); }); test.concurrent('in match', async () => { @@ -683,16 +1257,312 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { continue }')); - }); - - test.concurrent('in function', async () => { - assert.rejects(() => exe(` - for 1 { - @f() { - continue; - } - } - `)); - }); + await assert.rejects(() => exe('<: if true { continue }')); + }); + + test.concurrent('in function', async () => { + await assert.rejects(() => exe(` + for 1 { + @f() { + continue; + } + } + `)); + }); + + test.concurrent('invalid label', async () => { + await assert.rejects(() => exe(` + for 1 { + continue #l + } + `)); + }); + + describe('labeled each', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + test.concurrent('continue corresponding to each is not allowed in the target', async () => { + await assert.rejects( + () => exe('#l: each let i, eval { continue #l } {}'), + new AiScriptSyntaxError('continue corresponding to each is not allowed in the target', { line: 1, column: 24 }), + ); + }); + + test.concurrent('continue corresponding to for is not allowed in the count', async () => { + await assert.rejects( + () => exe('#l: for eval { continue #l } {}'), + new AiScriptSyntaxError('continue corresponding to for is not allowed in the count', { line: 1, column: 16 }), + ); + }); + + describe('continue corresponding to for is not allowed in the range', () => { + test.concurrent('from', async () => { + await assert.rejects( + () => exe('#l: for let i = eval { continue #l }, 0 {}'), + new AiScriptSyntaxError('continue corresponding to for is not allowed in the range', { line: 1, column: 24 }), + ); + }); + + test.concurrent('to', async () => { + await assert.rejects( + () => exe('#l: for let i = 0, eval { continue #l } {}'), + new AiScriptSyntaxError('continue corresponding to for is not allowed in the range', { line: 1, column: 27 }), + ); + }); + }); + + describe('labeled for', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); +}); + +describe('label', () => { + test.concurrent('invalid statement', async () => { + await assert.rejects( + () => exe('#l: null'), + new AiScriptSyntaxError('cannot use label for statement other than eval / if / match / for / each / while / do-while / loop', { line: 1, column: 5 }), + ); + }); + + test.concurrent('invalid expression', async () => { + await assert.rejects( + () => exe('let a = #l: null'), + new AiScriptSyntaxError('cannot use label for expression other than eval / if / match', { line: 1, column: 13 }), + ); + }); + + test.concurrent('invalid space', async () => { + await assert.rejects( + () => exe('# l: eval { null }'), + new AiScriptSyntaxError('cannot use spaces in a label', { line: 1, column: 3 }), + ); + await assert.rejects( + () => exe('#l: eval { break # l }'), + new AiScriptSyntaxError('cannot use spaces in a label', { line: 1, column: 20 }), + ); + }); }); diff --git a/test/keywords.ts b/test/keywords.ts index c318b3c5..983b8ea1 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -111,6 +111,35 @@ const sampleCodes = Object.entries<(word: string) => string>({ ### ${word} 1 `, + for: word => + ` + #${word}: for 1 {} + `, + + each: word => + ` + #${word}: each let v, [0] {} + `, + + while: word => + ` + #${word}: while false {} + `, + + break: word => + ` + #${word}: for 1 { + break #${word} + } + `, + + continue: word => + ` + #${word}: for 1 { + continue #${word} + } + `, + typeParam: word => ` @f<${word}>(x): ${word} { x } diff --git a/test/syntax.ts b/test/syntax.ts index f837929c..628dd6cf 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -784,6 +784,17 @@ describe('for', () => { `); }); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: for (let i, 10) { + count += i + 1 + } + <: count + `); + eq(res, NUM(55)); + }); }); describe('each', () => { @@ -841,6 +852,17 @@ describe('each', () => { } assert.fail(); }); + + test.concurrent('with label', async () => { + const res = await exe(` + let msgs = [] + #label: each let item, ["ai", "chan", "kawaii"] { + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); }); describe('while', () => { @@ -863,6 +885,17 @@ describe('while', () => { `); eq(res, NULL); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: while count < 42 { + count += 1 + } + <: count + `); + eq(res, NUM(42)); + }); }); describe('do-while', () => { @@ -885,6 +918,17 @@ describe('do-while', () => { `); eq(res, STR('hoge')); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + do { + count += 1 + } while count < 42 + <: count + `); + eq(res, NUM(42)); + }); }); describe('loop', () => { @@ -914,6 +958,18 @@ describe('loop', () => { `); eq(res, ARR([STR('ai'), STR('kawaii')])); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: loop { + if (count == 10) break + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); }); /* diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md index eb1c1233..6a6fcc37 100644 --- a/unreleased/jump-statements.md +++ b/unreleased/jump-statements.md @@ -1,6 +1,8 @@ -- **Breaking Change** return文、break文、continue文の挙動が変更されました。 +- return文、break文、continue文の挙動が変更されました。 + - Fix: eval式やif式内でreturn文あるいはbreak文、continue文を使用すると不正な値が取り出せる不具合を修正しました。 - return文は関数スコープ内でないと文法エラーになります。 - - break文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 + - ラベルが省略されたbreak文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 - return文は常に関数から脱出します。 - - break文は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 - - continue文は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 + - ラベルが省略されたbreak文は必ず最も内側の反復処理文の処理を中断し、ループから脱出します。 + - continue文は必ず最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 + - eval, if, match, loop, while, do-while, for, eachにラベルを付けてbreak文やcontinue文で指定したブロックから脱出できるようになります。eval, if, matchから脱出するbreak文には値を指定することができます。