From b59659528c9a7dfe844f146392a386c4544db557 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 12:23:55 +0900 Subject: [PATCH 01/18] =?UTF-8?q?=E7=B9=B0=E3=82=8A=E8=BF=94=E3=81=97?= =?UTF-8?q?=E5=87=A6=E7=90=86=E6=96=87=E3=81=AB=E3=83=A9=E3=83=99=E3=83=AB?= =?UTF-8?q?=E3=82=92=E3=81=A4=E3=81=91=E3=82=89=E3=82=8C=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 3 ++ src/node.ts | 3 ++ src/parser/scanner.ts | 2 +- src/parser/syntaxes/statements.ts | 48 ++++++++++++++++++++++++++ src/parser/token.ts | 2 ++ test/syntax.ts | 56 +++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index f7b63bf4f..38470ded8 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -262,6 +262,7 @@ type Div = NodeBase & { // @public (undocumented) type Each = NodeBase & { type: 'each'; + label?: string; var: Expression; items: Expression; for: Statement | Expression; @@ -343,6 +344,7 @@ type FnTypeSource = NodeBase & { // @public (undocumented) type For = NodeBase & { type: 'for'; + label?: string; var?: string; from?: Expression; to?: Expression; @@ -475,6 +477,7 @@ type Loc = { // @public (undocumented) type Loop = NodeBase & { type: 'loop'; + label?: string; statements: (Statement | Expression)[]; }; diff --git a/src/node.ts b/src/node.ts index 19e94e975..f40dc3004 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,6 +91,7 @@ export type For = NodeBase & { export type Loop = NodeBase & { type: 'loop'; // loop文 + label?: string; // ラベル statements: (Statement | Expression)[]; // 処理 }; diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 401f2bd3c..f2b611f7d 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/statements.ts b/src/parser/syntaxes/statements.ts index 5097313f7..231ca4447 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -31,6 +31,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); } @@ -198,6 +201,51 @@ 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 { + s.expect(TokenKind.Sharp); + s.next(); + + s.expect(TokenKind.Identifier); + const label = s.getTokenValue(); + s.next(); + + s.expect(TokenKind.Colon); + s.next(); + + const statement = parseLoopStatement(s); + statement.label = label; + return statement; +} + +function parseLoopStatement(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop { + const tokenKind = s.getTokenKind(); + switch (tokenKind) { + case TokenKind.EachKeyword: { + return parseEach(s); + } + case TokenKind.ForKeyword: { + return parseFor(s); + } + case TokenKind.LoopKeyword: { + return parseLoop(s); + } + case TokenKind.DoKeyword: { + return parseDoWhile(s); + } + case TokenKind.WhileKeyword: { + return parseWhile(s); + } + default: { + throw unexpectedTokenError(tokenKind, s.getPos()); + } + } +} + /** * ```abnf * Each = "each" "(" "let" Dest "," Expr ")" BlockOrStatement diff --git a/src/parser/token.ts b/src/parser/token.ts index d4bdaf49f..7e633b335 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/syntax.ts b/test/syntax.ts index e6b3b938d..c11f59100 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -774,6 +774,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', () => { @@ -831,6 +842,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', () => { @@ -853,6 +875,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', () => { @@ -875,6 +908,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', () => { @@ -904,6 +948,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)); + }); }); /* From 4bdded158f8126060488570cfb217b541d6d4faa Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 13:14:09 +0900 Subject: [PATCH 02/18] =?UTF-8?q?break,continue=E3=81=AB=E3=83=A9=E3=83=99?= =?UTF-8?q?=E3=83=AB=E3=82=92=E3=81=A4=E3=81=91=E3=82=89=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 2 + src/interpreter/control.ts | 8 +- src/interpreter/index.ts | 32 +- src/node.ts | 2 + src/parser/syntaxes/statements.ts | 52 ++- test/jump-statements.ts | 632 ++++++++++++++++++++++++++++++ 6 files changed, 720 insertions(+), 8 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 38470ded8..3e79323fe 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -228,6 +228,7 @@ type Bool = NodeBase & { // @public (undocumented) type Break = NodeBase & { type: 'break'; + label?: string; }; // @public (undocumented) @@ -240,6 +241,7 @@ type Call = NodeBase & { // @public (undocumented) type Continue = NodeBase & { type: 'continue'; + label?: string; }; // @public (undocumented) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index e748344ea..3bbba93e4 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -9,11 +9,13 @@ export type CReturn = { export type CBreak = { type: 'break'; + label?: string; value: null; }; export type CContinue = { type: 'continue'; + label?: string; value: null; }; @@ -25,13 +27,15 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ value: v, }); -export const BREAK = (): CBreak => ({ +export const BREAK = (label?: string): CBreak => ({ type: 'break' as const, + label, value: null, }); -export const CONTINUE = (): CContinue => ({ +export const CONTINUE = (label?: string): CContinue => ({ type: 'continue' as const, + label, value: null, }); diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 2d89264f6..a4669c86e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -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; } @@ -729,12 +757,12 @@ export class Interpreter { case 'break': { this.log('block:break', { scope: scope.name }); - return BREAK(); + return BREAK(node.label); } 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 f40dc3004..dd8f49c0e 100644 --- a/src/node.ts +++ b/src/node.ts @@ -97,10 +97,12 @@ export type Loop = NodeBase & { export type Break = NodeBase & { type: 'break'; // break文 + label?: string; // ラベル }; export type Continue = NodeBase & { type: 'continue'; // continue文 + label?: string; // ラベル }; export type AddAssign = NodeBase & { diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 231ca4447..c25c83479 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -50,12 +50,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); @@ -509,6 +507,52 @@ function parseWhile(s: ITokenStream): Ast.Loop { }, startPos, s.getPos()); } +/** + * ```abnf + * Break = "break" ["#" IDENT] + * ``` +*/ +function parseBreak(s: ITokenStream): Ast.Break { + const startPos = s.getPos(); + + s.expect(TokenKind.BreakKeyword); + s.next(); + + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + s.next(); + + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); + } + + return NODE('break', { label }, 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)) { + s.next(); + + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); + } + + return NODE('continue', { label }, startPos, s.getPos()); +} + /** * ```abnf * Assign = Expr ("=" / "+=" / "-=") Expr diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 80470d9a1..a7897ffcc 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -624,6 +624,399 @@ describe('break', () => { } `)); }); + + test.concurrent('invalid label', async () => { + assert.rejects(() => exe(` + for 1 { + break #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 + 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('continue', () => { @@ -695,4 +1088,243 @@ describe('continue', () => { } `)); }); + + test.concurrent('invalid label', async () => { + 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)); + }); + }); + + 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)); + }); + }); }); From 31c272f72dc6d1742124b3f9b9b35edb0c114ae0 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 13:47:33 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=E9=9D=99=E7=9A=84=E3=81=AA=E6=95=B4?= =?UTF-8?q?=E5=90=88=E6=80=A7=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/validate-jump-statements.ts | 21 ++++++++++---- src/parser/plugins/validate-keyword.ts | 24 +++++++++++++++ test/keywords.ts | 29 +++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index d1a346e21..3ca509590 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -3,13 +3,18 @@ import { AiScriptSyntaxError } from '../../error.js'; import type * as Ast from '../../node.js'; -function isInLoopScope(ancestors: Ast.Node[]): boolean { +function isInValidLoopScope(ancestors: Ast.Node[], label?: string): boolean { 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': + case 'each': { + if (label != null && label !== ancestor.label) { + continue; + } return true; + } case 'fn': return false; } @@ -26,13 +31,19 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (!isInLoopScope(ancestors)) { + if (!isInValidLoopScope(ancestors, node.label)) { + if (node.label != null) { + throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); + } throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); } break; } case 'continue': { - if (!isInLoopScope(ancestors)) { + if (!isInValidLoopScope(ancestors, node.label)) { + 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); } break; diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 024a1dea6..7e06ce61e 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -101,15 +101,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': { for (const param of node.params) { validateDest(param.dest); diff --git a/test/keywords.ts b/test/keywords.ts index ca9fca109..d69ca90f0 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -110,6 +110,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} + } + `, }); const parser = new Parser(); From 30b305b18f267657958b67fc744013a220a65af4 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 14 Dec 2024 22:39:50 +0900 Subject: [PATCH 04/18] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=87=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E3=82=BF=E3=83=96=E3=81=AB=E5=A4=89=E6=8F=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/jump-statements.ts | 2334 +++++++++++++++++++-------------------- 1 file changed, 1167 insertions(+), 1167 deletions(-) diff --git a/test/jump-statements.ts b/test/jump-statements.ts index a7897ffcc..3b4c265f4 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -31,528 +31,528 @@ describe('return', () => { }); 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)); + 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 } }')) - }); + 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 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('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', () => { @@ -615,408 +615,408 @@ describe('break', () => { assert.rejects(() => exe('<: if true { break }')); }); - test.concurrent('in function', async () => { - assert.rejects(() => exe(` - for 1 { - @f() { - break; - } - } - `)); - }); - - test.concurrent('invalid label', async () => { - assert.rejects(() => exe(` - for 1 { - break #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 - 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)); - }); - }); + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + break; + } + } + `)); + }); + + test.concurrent('invalid label', async () => { + assert.rejects(() => exe(` + for 1 { + break #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 + 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('continue', () => { @@ -1079,252 +1079,252 @@ describe('continue', () => { assert.rejects(() => exe('<: if true { continue }')); }); - test.concurrent('in function', async () => { - assert.rejects(() => exe(` - for 1 { - @f() { - continue; - } - } - `)); - }); - - test.concurrent('invalid label', async () => { - 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)); - }); - }); - - 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)); - }); - }); + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + continue; + } + } + `)); + }); + + test.concurrent('invalid label', async () => { + 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)); + }); + }); + + 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)); + }); + }); }); From 1e29f27384351a1b34fbe8d5a2c29415adbda73c Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 14 Dec 2024 23:45:51 +0900 Subject: [PATCH 05/18] =?UTF-8?q?if,match,eval=E3=81=8B=E3=82=89break?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/control.ts | 22 ++++++++++- src/interpreter/index.ts | 22 +++++------ src/node.ts | 3 ++ .../plugins/validate-jump-statements.ts | 32 ++++++++++++---- src/parser/syntaxes/expressions.ts | 33 ++++++++++++++++ src/parser/syntaxes/statements.ts | 36 ++++++------------ test/jump-statements.ts | 38 +++++++++++++++++++ 7 files changed, 143 insertions(+), 43 deletions(-) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 3bbba93e4..7b70c0d7b 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -1,6 +1,6 @@ import { AiScriptRuntimeError } from '../error.js'; import type { Reference } from './reference.js'; -import type { Value } from './value.js'; +import { NULL, type Value } from './value.js'; export type CReturn = { type: 'return'; @@ -39,6 +39,26 @@ export const CONTINUE = (label?: string): CContinue => ({ value: null, }); +/** + * 値がbreakで、ラベルが指定されていないまたは一致する場合のみ、その中身を取り出します。 + */ +export function unWrapBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && (v.label == null || v.label === label)) { + return NULL; + } + return v; +} + +/** + * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 + */ +export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && v.label != null && v.label === label) { + return 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 a4669c86e..a1b1e4dff 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'; @@ -367,24 +367,24 @@ export class Interpreter { case 'if': { const cond = await this._eval(node.cond, scope, callStack); if (isControl(cond)) { - return cond; + return unWrapLabeledBreak(cond, node.label); } 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); if (isControl(cond)) { - return cond; + return unWrapLabeledBreak(cond, node.label); } 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; } @@ -392,19 +392,19 @@ export class Interpreter { case 'match': { const about = await this._eval(node.about, scope, callStack); if (isControl(about)) { - return about; + return unWrapLabeledBreak(about, node.label); } for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); if (isControl(q)) { - return q; + return unWrapLabeledBreak(q, node.label); } 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; } @@ -723,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': { diff --git a/src/node.ts b/src/node.ts index dd8f49c0e..52a5f4140 100644 --- a/src/node.ts +++ b/src/node.ts @@ -270,6 +270,7 @@ export type Or = NodeBase & { export type If = NodeBase & { type: 'if'; // if式 + label?: string; // ラベル cond: Expression; // 条件式 then: Statement | Expression; // then節 elseif: { @@ -293,6 +294,7 @@ export type Fn = NodeBase & { export type Match = NodeBase & { type: 'match'; // パターンマッチ + label?: string; // ラベル about: Expression; // 対象 qs: { q: Expression; // 条件 @@ -303,6 +305,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 3ca509590..699399a76 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -3,7 +3,7 @@ import { AiScriptSyntaxError } from '../../error.js'; import type * as Ast from '../../node.js'; -function isInValidLoopScope(ancestors: Ast.Node[], label?: string): 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--) { const ancestor = ancestors[i]!; switch (ancestor.type) { @@ -13,13 +13,21 @@ function isInValidLoopScope(ancestors: Ast.Node[], label?: string): boolean { if (label != null && label !== ancestor.label) { continue; } - return true; + 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 { @@ -31,20 +39,30 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (!isInValidLoopScope(ancestors, node.label)) { + if (getCorrespondingBlock(ancestors, node.label) == null) { if (node.label != null) { throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); } - throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); + throw new AiScriptSyntaxError('unlabeled break must be inside for / each / while / do-while / loop', node.loc.start); } break; } case 'continue': { - if (!isInValidLoopScope(ancestors, node.label)) { + 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 '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/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 8ef6b356c..c972cb5e2 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -287,6 +287,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { s.next(); return expr; } + case TokenKind.Sharp: { + return parseExprWithLabel(s); + } } throw unexpectedTokenError(s.getTokenKind(), startPos); } @@ -342,6 +345,36 @@ 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 { + s.expect(TokenKind.Sharp); + s.next(); + + s.expect(TokenKind.Identifier); + const label = s.getTokenValue(); + s.next(); + + 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', s.getPos()); + } + } +} + /** * ```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 c25c83479..f5c0006c3 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -204,7 +204,7 @@ function parseOut(s: ITokenStream): Ast.Call { * StatementWithLabel = "#" IDENT ":" Statement * ``` */ -function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop { +function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop | Ast.If | Ast.Match | Ast.Block { s.expect(TokenKind.Sharp); s.next(); @@ -215,31 +215,19 @@ function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop s.expect(TokenKind.Colon); s.next(); - const statement = parseLoopStatement(s); - statement.label = label; - return statement; -} - -function parseLoopStatement(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop { - const tokenKind = s.getTokenKind(); - switch (tokenKind) { - case TokenKind.EachKeyword: { - return parseEach(s); - } - case TokenKind.ForKeyword: { - return parseFor(s); - } - case TokenKind.LoopKeyword: { - return parseLoop(s); - } - case TokenKind.DoKeyword: { - return parseDoWhile(s); - } - case TokenKind.WhileKeyword: { - return parseWhile(s); + 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 unexpectedTokenError(tokenKind, s.getPos()); + throw new AiScriptSyntaxError('cannot use label for statement other than eval / if / match / for / each / while / do-while / loop', s.getPos()); } } } diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 3b4c265f4..2859f1006 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -1017,6 +1017,44 @@ describe('break', () => { 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); + }); +}); + +describe('labeled match', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: match 0 { + default => { + break #l + 2 + } + } + `); + eq(res, NULL); + }); +}); + +describe('labeled eval', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: eval { + break #l + 2 + } + `); + eq(res, NULL); + }); +}); }); describe('continue', () => { From c166339ec0428129619d227de816ebde35a231f2 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 15 Dec 2024 01:07:41 +0900 Subject: [PATCH 06/18] =?UTF-8?q?if,match,eval=E3=81=AEbreak=E3=81=AB?= =?UTF-8?q?=E5=80=A4=E3=82=92=E6=8C=87=E5=AE=9A=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/control.ts | 13 +++-- src/interpreter/index.ts | 10 +++- src/node.ts | 1 + src/parser/syntaxes/statements.ts | 25 ++++++++- test/jump-statements.ts | 92 +++++++++++++++++++++---------- 5 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 7b70c0d7b..1ef47618c 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -1,6 +1,7 @@ import { AiScriptRuntimeError } from '../error.js'; +import { NULL } from './value.js'; import type { Reference } from './reference.js'; -import { NULL, type Value } from './value.js'; +import type { Value } from './value.js'; export type CReturn = { type: 'return'; @@ -10,7 +11,7 @@ export type CReturn = { export type CBreak = { type: 'break'; label?: string; - value: null; + value?: Value; }; export type CContinue = { @@ -27,10 +28,10 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ value: v, }); -export const BREAK = (label?: string): CBreak => ({ +export const BREAK = (label?: string, value?: CBreak['value']): CBreak => ({ type: 'break' as const, label, - value: null, + value: value, }); export const CONTINUE = (label?: string): CContinue => ({ @@ -44,7 +45,7 @@ export const CONTINUE = (label?: string): CContinue => ({ */ export function unWrapBreak(v: Value | Control, label: string | undefined): Value | Control { if (v.type === 'break' && (v.label == null || v.label === label)) { - return NULL; + return v.value ?? NULL; } return v; } @@ -54,7 +55,7 @@ export function unWrapBreak(v: Value | Control, label: string | undefined): Valu */ export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { if (v.type === 'break' && v.label != null && v.label === label) { - return NULL; + return v.value ?? NULL; } return v; } diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index a1b1e4dff..f64934ef4 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -756,8 +756,16 @@ 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(node.label); + return BREAK(node.label, val); } case 'continue': { diff --git a/src/node.ts b/src/node.ts index 52a5f4140..b346afa29 100644 --- a/src/node.ts +++ b/src/node.ts @@ -98,6 +98,7 @@ export type Loop = NodeBase & { export type Break = NodeBase & { type: 'break'; // break文 label?: string; // ラベル + expr?: Expression; // 式 }; export type Continue = NodeBase & { diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index f5c0006c3..0655e81f7 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -497,7 +497,7 @@ function parseWhile(s: ITokenStream): Ast.Loop { /** * ```abnf - * Break = "break" ["#" IDENT] + * Break = "break" ["#" IDENT [Expr]] * ``` */ function parseBreak(s: ITokenStream): Ast.Break { @@ -507,15 +507,20 @@ function parseBreak(s: ITokenStream): Ast.Break { s.next(); let label: string | undefined; + let expr: Ast.Expression | undefined; if (s.is(TokenKind.Sharp)) { s.next(); s.expect(TokenKind.Identifier); label = s.getTokenValue(); s.next(); + + if (!isStatementTerminator(s)) { + expr = parseExpr(s, false); + } } - return NODE('break', { label }, startPos, s.getPos()); + return NODE('break', { label, expr }, startPos, s.getPos()); } /** @@ -571,3 +576,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/test/jump-statements.ts b/test/jump-statements.ts index 2859f1006..ba2c325d0 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -1018,44 +1018,76 @@ describe('break', () => { }); }); -describe('labeled if', () => { - test.concurrent('simple break', async () => { - const res = await exe(` - <: #l: if true { - break #l - 2 - } - `); - eq(res, NULL); + 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 => { + 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); - }); -}); + `); + eq(res, NULL); + }); -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)); + }); }); }); -}); describe('continue', () => { test.concurrent('as statement', async () => { From 44e17c65655afd6f33579d5aecc318c7dbd4ac76 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 15 Dec 2024 01:15:43 +0900 Subject: [PATCH 07/18] =?UTF-8?q?if,match,eval=E4=BB=A5=E5=A4=96=E3=81=AEb?= =?UTF-8?q?reak=E3=81=AB=E5=80=A4=E3=81=8C=E6=8C=87=E5=AE=9A=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E3=81=A8=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/plugins/validate-jump-statements.ts | 12 +++++++++++- test/jump-statements.ts | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index 699399a76..adc22292c 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -39,11 +39,21 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (getCorrespondingBlock(ancestors, node.label) == null) { + 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); + } else 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; } diff --git a/test/jump-statements.ts b/test/jump-statements.ts index ba2c325d0..649ffdf2a 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', () => { @@ -633,6 +633,14 @@ describe('break', () => { `)); }); + test.concurrent('invalid value', async () => { + assert.rejects(() => exe(` + #l: for 1 { + break #l 1 + } + `), new AiScriptSyntaxError('break corresponding to statement cannot include value', { line: 3, column: 4 })); + }); + describe('labeled each', () => { test.concurrent('inner each', async () => { const res = await exe(` From f9b2e4cba9698bf93d00b3f5febc60a6dc1d726b Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 15 Dec 2024 01:40:03 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=A8=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/control.ts | 10 -- src/parser/syntaxes/expressions.ts | 2 +- src/parser/syntaxes/statements.ts | 2 +- test/jump-statements.ts | 162 ++++++++++++++++++----------- 4 files changed, 105 insertions(+), 71 deletions(-) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 1ef47618c..91fe5aaf0 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -40,16 +40,6 @@ export const CONTINUE = (label?: string): CContinue => ({ value: null, }); -/** - * 値がbreakで、ラベルが指定されていないまたは一致する場合のみ、その中身を取り出します。 - */ -export function unWrapBreak(v: Value | Control, label: string | undefined): Value | Control { - if (v.type === 'break' && (v.label == null || v.label === label)) { - return v.value ?? NULL; - } - return v; -} - /** * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 */ diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index c972cb5e2..40932ea2a 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -370,7 +370,7 @@ function parseExprWithLabel(s: ITokenStream): Ast.If | Ast.Match | Ast.Block { return expr; } default: { - throw new AiScriptSyntaxError('cannot use label for expression other than eval / if / match', s.getPos()); + throw new AiScriptSyntaxError('cannot use label for expression other than eval / if / match', expr.loc.start); } } } diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 0655e81f7..bd76b869d 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -227,7 +227,7 @@ function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop return statement; } default: { - throw new AiScriptSyntaxError('cannot use label for statement other than eval / if / match / for / each / while / do-while / loop', s.getPos()); + throw new AiScriptSyntaxError('cannot use label for statement other than eval / if / match / for / each / while / do-while / loop', statement.loc.start); } } } diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 649ffdf2a..9db8e77dd 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -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,7 +27,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 }')); + await assert.rejects(() => exe('<: eval { return 1 }')); }); describe('in if', () => { @@ -39,7 +39,7 @@ describe('return', () => { <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if eval { return true } {}')); + await assert.rejects(() => exe('<: if eval { return true } {}')); }); test.concurrent('then', async () => { @@ -52,7 +52,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: if true { return 1 }')); + await assert.rejects(() => exe('<: if true { return 1 }')); }); test.concurrent('elif cond', async () => { @@ -63,7 +63,7 @@ describe('return', () => { <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); + await assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); }); test.concurrent('elif then', async () => { @@ -77,7 +77,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: if false {} elif true eval { return true }')); + await assert.rejects(() => exe('<: if false {} elif true eval { return true }')); }); test.concurrent('else', async () => { @@ -91,7 +91,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: if false {} else eval { return true }')); + await assert.rejects(() => exe('<: if false {} else eval { return true }')); }); }); @@ -104,7 +104,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: match eval { return 1 } {}')); + await assert.rejects(() => exe('<: match eval { return 1 } {}')); }); test.concurrent('case q', async () => { @@ -119,7 +119,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) + await assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) }); test.concurrent('case a', async () => { @@ -134,7 +134,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: match 0 { case 0 => { return 1 } }')) + await assert.rejects(() => exe('<: match 0 { case 0 => { return 1 } }')) }); test.concurrent('default', async () => { @@ -149,7 +149,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) + await assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) }); }); @@ -162,7 +162,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 } + 2')); + await assert.rejects(() => exe('<: eval { return 1 } + 2')); }); test.concurrent('right', async () => { @@ -173,7 +173,7 @@ describe('return', () => { <: f() `); eq(res, NUM(2)); - assert.rejects(() => exe('<: 1 + eval { return 2 }')); + await assert.rejects(() => exe('<: 1 + eval { return 2 }')); }); }); @@ -186,7 +186,7 @@ describe('return', () => { f()('Hi') `); eq(res, STR('Hi')); - assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); + await assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); }); test.concurrent('arg', async () => { @@ -197,7 +197,7 @@ describe('return', () => { <: f() `); eq(res, STR('Hello, world!')); - assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) + await assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) }); }); @@ -210,7 +210,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for eval { return 1 } {}')); + await assert.rejects(() => exe('for eval { return 1 } {}')); }); test.concurrent('from', async () => { @@ -221,7 +221,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); + await assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); }); test.concurrent('to', async () => { @@ -232,7 +232,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); + await assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); }); test.concurrent('for', async () => { @@ -245,7 +245,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for 1 { return 1 }')); + await assert.rejects(() => exe('for 1 { return 1 }')); }) }); @@ -258,7 +258,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); + await assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); }); test.concurrent('for', async () => { @@ -271,7 +271,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('each let v, [0] { return 1 }')); + await assert.rejects(() => exe('each let v, [0] { return 1 }')); }); }); @@ -285,7 +285,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = null; a = eval { return 1 }')); + await assert.rejects(() => exe('let a = null; a = eval { return 1 }')); }); test.concurrent('index target', async () => { @@ -297,7 +297,7 @@ describe('return', () => { <: f() `); eq(res, ARR([NULL])); - assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); + await assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); }); test.concurrent('index', async () => { @@ -309,7 +309,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); + await assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); }); test.concurrent('prop target', async () => { @@ -321,7 +321,7 @@ describe('return', () => { <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); + await assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); }); test.concurrent('arr', async () => { @@ -333,7 +333,7 @@ describe('return', () => { <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); + await assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); }); test.concurrent('obj', async () => { @@ -345,7 +345,7 @@ describe('return', () => { <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); + await assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); }); }); @@ -359,7 +359,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); + await assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); }); test.concurrent('expr', async () => { @@ -371,7 +371,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); + await assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); }); }); @@ -385,7 +385,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); + await assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); }); test.concurrent('expr', async () => { @@ -397,7 +397,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); + await assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); }); }); @@ -409,7 +409,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }]')); + await assert.rejects(() => exe('<: [eval { return 1 }]')); }); test.concurrent('in object', async () => { @@ -422,7 +422,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }')); + await assert.rejects(() => exe('<: { p: eval { return 1 } }')); }); test.concurrent('in prop', async () => { @@ -435,7 +435,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); + await assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); }); describe('in index', () => { @@ -447,7 +447,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }][0]')); + await assert.rejects(() => exe('<: [eval { return 1 }][0]')); }); test.concurrent('index', async () => { @@ -458,7 +458,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: [0][eval { return 1 }]')); + await assert.rejects(() => exe('<: [0][eval { return 1 }]')); }); }); @@ -470,7 +470,7 @@ describe('return', () => { <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: !eval { return true }')); + await assert.rejects(() => exe('<: !eval { return true }')); }); test.concurrent('in function default param', async () => { @@ -481,7 +481,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); + await assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); }); test.concurrent('in template', async () => { @@ -492,7 +492,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: `{eval {return 1}}`')); + await assert.rejects(() => exe('<: `{eval {return 1}}`')); }); test.concurrent('in return', async () => { @@ -503,7 +503,24 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('return eval { return 1 } + 2')); + 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 () => { @@ -515,7 +532,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } && false')); + await assert.rejects(() => exe('eval { return 1 } && false')); }); test.concurrent('right', async () => { @@ -526,7 +543,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('true && eval { return 1 }')); + await assert.rejects(() => exe('true && eval { return 1 }')); }); }); @@ -539,7 +556,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } || false')); + await assert.rejects(() => exe('eval { return 1 } || false')); }); test.concurrent('right', async () => { @@ -550,7 +567,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('false || eval { return 1 }')); + await assert.rejects(() => exe('false || eval { return 1 }')); }); }); }); @@ -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,11 +629,11 @@ describe('break', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { break }')); + await assert.rejects(() => exe('<: if true { break }')); }); test.concurrent('in function', async () => { - assert.rejects(() => exe(` + await assert.rejects(() => exe(` for 1 { @f() { break; @@ -626,7 +643,7 @@ describe('break', () => { }); test.concurrent('invalid label', async () => { - assert.rejects(() => exe(` + await assert.rejects(() => exe(` for 1 { break #l } @@ -634,7 +651,7 @@ describe('break', () => { }); test.concurrent('invalid value', async () => { - assert.rejects(() => exe(` + await assert.rejects(() => exe(` #l: for 1 { break #l 1 } @@ -1094,6 +1111,17 @@ describe('break', () => { `); 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)); + }); }); }); @@ -1108,8 +1136,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 () => { @@ -1124,7 +1152,7 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: eval { continue }')); + await assert.rejects(() => exe('<: eval { continue }')); }); test.concurrent('in if', async () => { @@ -1139,7 +1167,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 () => { @@ -1154,11 +1182,11 @@ describe('continue', () => { <: x `); eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { continue }')); + await assert.rejects(() => exe('<: if true { continue }')); }); test.concurrent('in function', async () => { - assert.rejects(() => exe(` + await assert.rejects(() => exe(` for 1 { @f() { continue; @@ -1168,7 +1196,7 @@ describe('continue', () => { }); test.concurrent('invalid label', async () => { - assert.rejects(() => exe(` + await assert.rejects(() => exe(` for 1 { continue #l } @@ -1406,3 +1434,19 @@ describe('continue', () => { }); }); }); + +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 }), + ); + }); +}); From b4404c3f962e46ef5b8dac20d5a037a5ed59fea0 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 15 Dec 2024 02:06:34 +0900 Subject: [PATCH 09/18] =?UTF-8?q?=E3=83=A9=E3=83=99=E3=83=AB=E3=81=AB?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/common.ts | 19 +++++++++++++++++++ src/parser/syntaxes/expressions.ts | 10 ++-------- src/parser/syntaxes/statements.ts | 22 ++++------------------ test/jump-statements.ts | 11 +++++++++++ 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index a9216823d..2d721813d 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -134,6 +134,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; +} + //#region Type export function parseType(s: ITokenStream): Ast.TypeSource { diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 40932ea2a..17778b833 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, parseType } from './common.js'; +import { parseBlock, parseLabel, parseOptionalSeparator, parseParams, parseType } from './common.js'; import { parseBlockOrStatement } from './statements.js'; import type * as Ast from '../../node.js'; @@ -351,13 +351,7 @@ function parseCall(s: ITokenStream, target: Ast.Expression): Ast.Call { * ``` */ function parseExprWithLabel(s: ITokenStream): Ast.If | Ast.Match | Ast.Block { - s.expect(TokenKind.Sharp); - s.next(); - - s.expect(TokenKind.Identifier); - const label = s.getTokenValue(); - s.next(); - + const label = parseLabel(s); s.expect(TokenKind.Colon); s.next(); diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index bd76b869d..c1e2cf595 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, parseType } from './common.js'; +import { parseBlock, parseDest, parseLabel, parseParams, parseType } from './common.js'; import { parseExpr } from './expressions.js'; import type * as Ast from '../../node.js'; @@ -205,13 +205,7 @@ function parseOut(s: ITokenStream): Ast.Call { * ``` */ function parseStatementWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop | Ast.If | Ast.Match | Ast.Block { - s.expect(TokenKind.Sharp); - s.next(); - - s.expect(TokenKind.Identifier); - const label = s.getTokenValue(); - s.next(); - + const label = parseLabel(s); s.expect(TokenKind.Colon); s.next(); @@ -509,11 +503,7 @@ function parseBreak(s: ITokenStream): Ast.Break { let label: string | undefined; let expr: Ast.Expression | undefined; if (s.is(TokenKind.Sharp)) { - s.next(); - - s.expect(TokenKind.Identifier); - label = s.getTokenValue(); - s.next(); + label = parseLabel(s); if (!isStatementTerminator(s)) { expr = parseExpr(s, false); @@ -536,11 +526,7 @@ function parseContinue(s: ITokenStream): Ast.Continue { let label: string | undefined; if (s.is(TokenKind.Sharp)) { - s.next(); - - s.expect(TokenKind.Identifier); - label = s.getTokenValue(); - s.next(); + label = parseLabel(s); } return NODE('continue', { label }, startPos, s.getPos()); diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 9db8e77dd..4e8e9542f 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -1449,4 +1449,15 @@ describe('label', () => { 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 }), + ); + }); }); From 31885c0f14ae8804aef1223f2824400677a1c248 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 25 Dec 2024 00:13:52 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=E9=81=95=E6=B3=95=E3=81=AAcontinue?= =?UTF-8?q?=E6=96=87=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/jump-statements.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 4e8e9542f..b6b12fae2 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -658,6 +658,21 @@ describe('break', () => { `), 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 }), + ); + }); + describe('labeled each', () => { test.concurrent('inner each', async () => { const res = await exe(` From e32c1f9b821aa4809f528f907d7a620332871590 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 25 Dec 2024 00:19:35 +0900 Subject: [PATCH 11/18] =?UTF-8?q?API=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 75bcb7ff5..c20a03e24 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)[]; }; @@ -231,6 +232,7 @@ type Bool = NodeBase & { type Break = NodeBase & { type: 'break'; label?: string; + expr?: Expression; }; // @public (undocumented) @@ -384,6 +386,7 @@ type Identifier = NodeBase & { // @public (undocumented) type If = NodeBase & { type: 'if'; + label?: string; cond: Expression; then: Statement | Expression; elseif: { @@ -504,6 +507,7 @@ type Lteq = NodeBase & { // @public (undocumented) type Match = NodeBase & { type: 'match'; + label?: string; about: Expression; qs: { q: Expression; From 541a1437c8565d08df841c436981e716f9c011cb Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 25 Dec 2024 00:37:07 +0900 Subject: [PATCH 12/18] CHANGELOG --- unreleased/jump-statements.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md index eb1c12335..3e39f0fba 100644 --- a/unreleased/jump-statements.md +++ b/unreleased/jump-statements.md @@ -1,6 +1,7 @@ -- **Breaking Change** return文、break文、continue文の挙動が変更されました。 +- 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文には値を指定することができます。 From fd8d4e09a0438d5579f81a468683d9516b33cc30 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 25 Dec 2024 01:55:10 +0900 Subject: [PATCH 13/18] =?UTF-8?q?break=E3=81=AE=E3=83=A9=E3=83=99=E3=83=AB?= =?UTF-8?q?=E3=82=92=E7=9C=81=E7=95=A5=E3=81=97=E3=81=A6=E3=83=A9=E3=83=99?= =?UTF-8?q?=E3=83=AB=E4=BB=98=E3=81=8D=E5=BC=8F=E3=82=92=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AE=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=82=92=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/statements.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 027f1a5c3..d8918adf9 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -514,7 +514,9 @@ function parseBreak(s: ITokenStream): Ast.Break { if (s.is(TokenKind.Sharp)) { label = parseLabel(s); - if (!isStatementTerminator(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); } } From d9b4ea6d17cd0411bf86a88dd5cf9b1c7aa95f44 Mon Sep 17 00:00:00 2001 From: Take-John Date: Tue, 31 Dec 2024 15:11:06 +0900 Subject: [PATCH 14/18] Update unreleased/jump-statements.md Co-authored-by: FineArchs <133759614+FineArchs@users.noreply.github.com> --- unreleased/jump-statements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md index 3e39f0fba..91fa77a9d 100644 --- a/unreleased/jump-statements.md +++ b/unreleased/jump-statements.md @@ -4,4 +4,4 @@ - return文は常に関数から脱出します。 - ラベルが省略されたbreak文は必ず最も内側の反復処理文の処理を中断し、ループから脱出します。 - continue文は必ず最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 - - eval, if, match, loop, while, do-while, for, eachにラベルを付けてbreak文やcontinue文で指定したブロックから脱出できるようになります。eval, if, matchから脱出するbreak文には値を指定することができます。 + - eval, if, match, loop, while, do-while, for, eachにラベルを付けてbreak文やcontinue文で指定したブロックから脱出できるようになります。eval, if, matchから脱出するbreak文には値を指定することができます。 From c1695fd7125eee674e8e8d8cdfa96efba2eb6581 Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 31 Dec 2024 17:26:12 +0900 Subject: [PATCH 15/18] =?UTF-8?q?=E3=83=90=E3=82=B0=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E3=81=AE=E6=96=87=E8=A8=80=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unreleased/jump-statements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md index 91fa77a9d..6a6fcc379 100644 --- a/unreleased/jump-statements.md +++ b/unreleased/jump-statements.md @@ -1,4 +1,5 @@ - return文、break文、continue文の挙動が変更されました。 + - Fix: eval式やif式内でreturn文あるいはbreak文、continue文を使用すると不正な値が取り出せる不具合を修正しました。 - return文は関数スコープ内でないと文法エラーになります。 - ラベルが省略されたbreak文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 - return文は常に関数から脱出します。 From c67f3d9476e7d9f393908d0ab3b2bbdbae16bbaa Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 5 Jan 2025 22:58:38 +0900 Subject: [PATCH 16/18] =?UTF-8?q?if=E3=81=AE=E6=9D=A1=E4=BB=B6=E3=82=84mat?= =?UTF-8?q?ch=E3=81=AE=E3=82=BF=E3=83=BC=E3=82=B2=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=81=A8=E3=83=91=E3=82=BF=E3=83=BC=E3=83=B3=E3=81=A7break?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 4 +- .../plugins/validate-jump-statements.ts | 36 ++++++++++++- test/jump-statements.ts | 51 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index c865c63f2..a0ed75b11 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -367,7 +367,7 @@ export class Interpreter { case 'if': { const cond = await this._eval(node.cond, scope, callStack); if (isControl(cond)) { - return unWrapLabeledBreak(cond, node.label); + return cond; } assertBoolean(cond); if (cond.value) { @@ -392,7 +392,7 @@ export class Interpreter { case 'match': { const about = await this._eval(node.about, scope, callStack); if (isControl(about)) { - return unWrapLabeledBreak(about, node.label); + return about; } for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index adc22292c..b731f6686 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -45,7 +45,41 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 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); - } else if (node.expr != null) { + } + + 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)) { + 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': diff --git a/test/jump-statements.ts b/test/jump-statements.ts index b6b12fae2..ba9d7e807 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -673,6 +673,57 @@ describe('break', () => { ); }); + 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 }), + ); + }); + }); + + test.concurrent('break corresponding to if is not allowed in the condition', 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('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(` From 2e22cfc4560ec0eb2bbc14964199bc9f3156df17 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 6 Jan 2025 22:29:12 +0900 Subject: [PATCH 17/18] =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E3=81=AA=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E3=81=A7=E3=81=AEcontinue=E3=82=92=E6=84=8F=E5=91=B3?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E3=81=A7=E5=BC=BE=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/validate-jump-statements.ts | 14 +++++++++ test/jump-statements.ts | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index b731f6686..bc01d2980 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -100,6 +100,20 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 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': diff --git a/test/jump-statements.ts b/test/jump-statements.ts index ba9d7e807..476729f47 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -1346,6 +1346,36 @@ describe('continue', () => { }); }); + 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(` From 74b1a65a8b30b9865215df26a7ff49a0a990cecb Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 7 Jan 2025 16:53:17 +0900 Subject: [PATCH 18/18] =?UTF-8?q?elseif=E3=81=AE=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E3=81=A8match=E3=81=AE=E3=83=91=E3=82=BF=E3=83=BC=E3=83=B3?= =?UTF-8?q?=E3=81=AB=E3=81=8A=E3=81=84=E3=81=A6break=E3=82=92unwrap?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 4 ++-- .../plugins/validate-jump-statements.ts | 2 +- test/jump-statements.ts | 19 ++++++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index a0ed75b11..8cdab2ae6 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -376,7 +376,7 @@ export class Interpreter { for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); if (isControl(cond)) { - return unWrapLabeledBreak(cond, node.label); + return cond; } assertBoolean(cond); if (cond.value) { @@ -397,7 +397,7 @@ export class Interpreter { for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); if (isControl(q)) { - return unWrapLabeledBreak(q, node.label); + return q; } if (eq(about, q)) { return unWrapLabeledBreak(await this._evalClause(qa.a, scope, callStack), node.label); diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index bc01d2980..9828bca9d 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -63,7 +63,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'if': { - if (ancestors.includes(block.cond)) { + 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; diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 476729f47..905898ddd 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -703,11 +703,20 @@ describe('break', () => { }); }); - test.concurrent('break corresponding to if is not allowed in the condition', 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 }), - ); + 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 () => {