From bc05b7697575def64d98cac59c3c9a4d0c5397aa Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 12:23:55 +0900 Subject: [PATCH 01/22] =?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 f7b63bf4..38470ded 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 19e94e97..f40dc300 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 401f2bd3..f2b611f7 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 5097313f..231ca444 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 d4bdaf49..7e633b33 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 e6b3b938..c11f5910 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 c9508def7c937d90d4882fc838520f5b8523a535 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 13:14:09 +0900 Subject: [PATCH 02/22] =?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 38470ded..3e79323f 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 e748344e..3bbba93e 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 2d89264f..a4669c86 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 f40dc300..dd8f49c0 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 231ca444..c25c8347 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 80470d9a..a7897ffc 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 7471f9f38b7104703b8b8862f83e2e3d5d1f94c4 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 13:47:33 +0900 Subject: [PATCH 03/22] =?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 | 31 ++++++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index d1a346e2..3ca50959 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 024a1dea..7e06ce61 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 1311fd47..737577f9 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -64,9 +64,38 @@ const sampleCodes = Object.entries({ ` ### ${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} + } + `, }); -function pickRandom(arr: T[]): T { +function pickRandom(arr: readonly T[]): T { return arr[Math.floor(Math.random() * arr.length)]; } From 27286b164c290e29ec3fa2bf8fc3b4af1bbf8763 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 15:48:53 +0900 Subject: [PATCH 04/22] =?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=A7=8B=E6=96=87=E3=82=92=E5=BC=8F=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node.ts | 6 +- src/parser/syntaxes/common.ts | 2 +- .../syntaxes/expression/control-flow.ts | 250 +++++++++++++++++ .../{expressions.ts => expression/index.ts} | 22 +- src/parser/syntaxes/statements.ts | 259 +----------------- src/parser/syntaxes/toplevel.ts | 2 +- test/syntax.ts | 35 +++ 7 files changed, 304 insertions(+), 272 deletions(-) create mode 100644 src/parser/syntaxes/expression/control-flow.ts rename src/parser/syntaxes/{expressions.ts => expression/index.ts} (96%) diff --git a/src/node.ts b/src/node.ts index dd8f49c0..c4a5d947 100644 --- a/src/node.ts +++ b/src/node.ts @@ -35,9 +35,6 @@ export type Meta = NodeBase & { export type Statement = Definition | Return | - Each | - For | - Loop | Break | Continue | Assign | @@ -126,6 +123,9 @@ export type Assign = NodeBase & { // expressions export type Expression = + Each | + For | + Loop | If | Fn | Match | diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index a9216823..b183a87f 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -2,7 +2,7 @@ import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { NODE } from '../utils.js'; import { parseStatement } from './statements.js'; -import { parseExpr } from './expressions.js'; +import { parseExpr } from './expression/index.js'; import type { ITokenStream } from '../streams/token-stream.js'; import type * as Ast from '../../node.js'; diff --git a/src/parser/syntaxes/expression/control-flow.ts b/src/parser/syntaxes/expression/control-flow.ts new file mode 100644 index 00000000..9dfc61aa --- /dev/null +++ b/src/parser/syntaxes/expression/control-flow.ts @@ -0,0 +1,250 @@ +/** + * 制御構造 + */ + +import { AiScriptSyntaxError } from '../../../error.js'; +import { TokenKind } from '../../token.js'; +import { unexpectedTokenError, NODE } from '../../utils.js'; +import { parseBlock, parseDest } from '../common.js'; +import { parseBlockOrStatement } from '../statements.js'; +import { parseExpr } from './index.js'; + +import type * as Ast from '../../../node.js'; +import type { ITokenStream } from '../../streams/token-stream.js'; + +/** + * ```abnf + * ExprWithLabel = "#" IDENT ":" Expression + * ``` +*/ +export function parseExprWithLabel(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 = parseControlFlowExpr(s); + statement.label = label; + return statement; +} + +export function parseControlFlowExpr(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 + * / "each" "let" Dest "," Expr BlockOrStatement + * ``` +*/ +function parseEach(s: ITokenStream): Ast.Each { + const startPos = s.getPos(); + let hasParen = false; + + s.expect(TokenKind.EachKeyword); + s.next(); + + if (s.is(TokenKind.OpenParen)) { + hasParen = true; + s.next(); + } + + s.expect(TokenKind.LetKeyword); + s.next(); + + const dest = parseDest(s); + + if (s.is(TokenKind.Comma)) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const items = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockOrStatement(s); + + return NODE('each', { + var: dest, + items: items, + for: body, + }, startPos, s.getPos()); +} + +/** + * ```abnf + * For = ForRange / ForTimes + * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" BlockOrStatement + * / "for" "let" IDENT ["=" Expr] "," Expr BlockOrStatement + * ForTimes = "for" "(" Expr ")" BlockOrStatement + * / "for" Expr BlockOrStatement + * ``` +*/ +function parseFor(s: ITokenStream): Ast.For { + const startPos = s.getPos(); + let hasParen = false; + + s.expect(TokenKind.ForKeyword); + s.next(); + + if (s.is(TokenKind.OpenParen)) { + hasParen = true; + s.next(); + } + + if (s.is(TokenKind.LetKeyword)) { + // range syntax + s.next(); + + const identPos = s.getPos(); + + s.expect(TokenKind.Identifier); + const name = s.getTokenValue(); + s.next(); + + let _from: Ast.Expression; + if (s.is(TokenKind.Eq)) { + s.next(); + _from = parseExpr(s, false); + } else { + _from = NODE('num', { value: 0 }, identPos, identPos); + } + + if (s.is(TokenKind.Comma)) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const to = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockOrStatement(s); + + return NODE('for', { + var: name, + from: _from, + to, + for: body, + }, startPos, s.getPos()); + } else { + // times syntax + + const times = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockOrStatement(s); + + return NODE('for', { + times, + for: body, + }, startPos, s.getPos()); + } +} + +/** + * ```abnf + * Loop = "loop" Block + * ``` +*/ +function parseLoop(s: ITokenStream): Ast.Loop { + const startPos = s.getPos(); + + s.expect(TokenKind.LoopKeyword); + s.next(); + const statements = parseBlock(s); + + return NODE('loop', { statements }, startPos, s.getPos()); +} + +/** + * ```abnf + * Loop = "do" BlockOrStatement "while" Expr + * ``` +*/ +function parseDoWhile(s: ITokenStream): Ast.Loop { + const doStartPos = s.getPos(); + s.expect(TokenKind.DoKeyword); + s.next(); + const body = parseBlockOrStatement(s); + const whilePos = s.getPos(); + s.expect(TokenKind.WhileKeyword); + s.next(); + const cond = parseExpr(s, false); + const endPos = s.getPos(); + + return NODE('loop', { + statements: [ + body, + NODE('if', { + cond: NODE('not', { expr: cond }, whilePos, endPos), + then: NODE('break', {}, endPos, endPos), + elseif: [], + }, whilePos, endPos), + ], + }, doStartPos, endPos); +} + +/** + * ```abnf + * Loop = "while" Expr BlockOrStatement + * ``` +*/ +function parseWhile(s: ITokenStream): Ast.Loop { + const startPos = s.getPos(); + s.expect(TokenKind.WhileKeyword); + s.next(); + const cond = parseExpr(s, false); + const condEndPos = s.getPos(); + const body = parseBlockOrStatement(s); + + return NODE('loop', { + statements: [ + NODE('if', { + cond: NODE('not', { expr: cond }, startPos, condEndPos), + then: NODE('break', {}, condEndPos, condEndPos), + elseif: [], + }, startPos, condEndPos), + body, + ], + }, startPos, s.getPos()); +} diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expression/index.ts similarity index 96% rename from src/parser/syntaxes/expressions.ts rename to src/parser/syntaxes/expression/index.ts index 8ef6b356..e510b647 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expression/index.ts @@ -1,12 +1,13 @@ -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 { parseBlockOrStatement } from './statements.js'; +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 { parseBlockOrStatement } from '../statements.js'; +import { parseControlFlowExpr, parseExprWithLabel } from './control-flow.js'; -import type * as Ast from '../../node.js'; -import type { ITokenStream } from '../streams/token-stream.js'; +import type * as Ast from '../../../node.js'; +import type { ITokenStream } from '../../streams/token-stream.js'; export function parseExpr(s: ITokenStream, isStatic: boolean): Ast.Expression { if (isStatic) { @@ -193,6 +194,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { const startPos = s.getPos(); switch (s.getTokenKind()) { + case TokenKind.Sharp: { + return parseExprWithLabel(s); + } case TokenKind.IfKeyword: { if (isStatic) break; return parseIf(s); @@ -288,7 +292,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { return expr; } } - throw unexpectedTokenError(s.getTokenKind(), startPos); + return parseControlFlowExpr(s); } /** diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index c25c8347..dc98be6d 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -2,14 +2,12 @@ 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 { parseExpr } from './expressions.js'; +import { parseExpr } from './expression/index.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression { - const startPos = s.getPos(); - switch (s.getTokenKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { @@ -31,24 +29,6 @@ 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); - } - case TokenKind.ForKeyword: { - return parseFor(s); - } - case TokenKind.LoopKeyword: { - return parseLoop(s); - } - case TokenKind.DoKeyword: { - return parseDoWhile(s); - } - case TokenKind.WhileKeyword: { - return parseWhile(s); - } case TokenKind.BreakKeyword: { return parseBreak(s); } @@ -199,175 +179,6 @@ 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 - * / "each" "let" Dest "," Expr BlockOrStatement - * ``` -*/ -function parseEach(s: ITokenStream): Ast.Each { - const startPos = s.getPos(); - let hasParen = false; - - s.expect(TokenKind.EachKeyword); - s.next(); - - if (s.is(TokenKind.OpenParen)) { - hasParen = true; - s.next(); - } - - s.expect(TokenKind.LetKeyword); - s.next(); - - const dest = parseDest(s); - - if (s.is(TokenKind.Comma)) { - s.next(); - } else { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - - const items = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('each', { - var: dest, - items: items, - for: body, - }, startPos, s.getPos()); -} - -/** - * ```abnf - * For = ForRange / ForTimes - * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" BlockOrStatement - * / "for" "let" IDENT ["=" Expr] "," Expr BlockOrStatement - * ForTimes = "for" "(" Expr ")" BlockOrStatement - * / "for" Expr BlockOrStatement - * ``` -*/ -function parseFor(s: ITokenStream): Ast.For { - const startPos = s.getPos(); - let hasParen = false; - - s.expect(TokenKind.ForKeyword); - s.next(); - - if (s.is(TokenKind.OpenParen)) { - hasParen = true; - s.next(); - } - - if (s.is(TokenKind.LetKeyword)) { - // range syntax - s.next(); - - const identPos = s.getPos(); - - s.expect(TokenKind.Identifier); - const name = s.getTokenValue(); - s.next(); - - let _from: Ast.Expression; - if (s.is(TokenKind.Eq)) { - s.next(); - _from = parseExpr(s, false); - } else { - _from = NODE('num', { value: 0 }, identPos, identPos); - } - - if (s.is(TokenKind.Comma)) { - s.next(); - } else { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - - const to = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('for', { - var: name, - from: _from, - to, - for: body, - }, startPos, s.getPos()); - } else { - // times syntax - - const times = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('for', { - times, - for: body, - }, startPos, s.getPos()); - } -} - /** * ```abnf * Return = "return" Expr @@ -439,74 +250,6 @@ function parseAttr(s: ITokenStream): Ast.Attribute { return NODE('attr', { name, value }, startPos, s.getPos()); } -/** - * ```abnf - * Loop = "loop" Block - * ``` -*/ -function parseLoop(s: ITokenStream): Ast.Loop { - const startPos = s.getPos(); - - s.expect(TokenKind.LoopKeyword); - s.next(); - const statements = parseBlock(s); - - return NODE('loop', { statements }, startPos, s.getPos()); -} - -/** - * ```abnf - * Loop = "do" BlockOrStatement "while" Expr - * ``` -*/ -function parseDoWhile(s: ITokenStream): Ast.Loop { - const doStartPos = s.getPos(); - s.expect(TokenKind.DoKeyword); - s.next(); - const body = parseBlockOrStatement(s); - const whilePos = s.getPos(); - s.expect(TokenKind.WhileKeyword); - s.next(); - const cond = parseExpr(s, false); - const endPos = s.getPos(); - - return NODE('loop', { - statements: [ - body, - NODE('if', { - cond: NODE('not', { expr: cond }, whilePos, endPos), - then: NODE('break', {}, endPos, endPos), - elseif: [], - }, whilePos, endPos), - ], - }, doStartPos, endPos); -} - -/** - * ```abnf - * Loop = "while" Expr BlockOrStatement - * ``` -*/ -function parseWhile(s: ITokenStream): Ast.Loop { - const startPos = s.getPos(); - s.expect(TokenKind.WhileKeyword); - s.next(); - const cond = parseExpr(s, false); - const condEndPos = s.getPos(); - const body = parseBlockOrStatement(s); - - return NODE('loop', { - statements: [ - NODE('if', { - cond: NODE('not', { expr: cond }, startPos, condEndPos), - then: NODE('break', {}, condEndPos, condEndPos), - elseif: [], - }, startPos, condEndPos), - body, - ], - }, startPos, s.getPos()); -} - /** * ```abnf * Break = "break" ["#" IDENT] diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index 91a07787..242b4638 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -2,7 +2,7 @@ import { NODE } from '../utils.js'; import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { parseDefStatement, parseStatement } from './statements.js'; -import { parseExpr } from './expressions.js'; +import { parseExpr } from './expression/index.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; diff --git a/test/syntax.ts b/test/syntax.ts index c11f5910..da16368a 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -785,6 +785,13 @@ describe('for', () => { `); eq(res, NUM(55)); }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: for 1 { "a" } + `); + eq(res, NULL); + }); }); describe('each', () => { @@ -853,6 +860,13 @@ describe('each', () => { `); eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: each let v, [0] { "a" } + `); + eq(res, NULL); + }); }); describe('while', () => { @@ -886,6 +900,13 @@ describe('while', () => { `); eq(res, NUM(42)); }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: while false { "a" } + `); + eq(res, NULL); + }); }); describe('do-while', () => { @@ -919,6 +940,13 @@ describe('do-while', () => { `); eq(res, NUM(42)); }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: do { "a" } while false + `); + eq(res, NULL); + }); }); describe('loop', () => { @@ -960,6 +988,13 @@ describe('loop', () => { `); eq(res, NUM(10)); }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: loop { break } + `); + eq(res, NULL); + }); }); /* From 60d13bc7252e08c5defef354896383dce10d39e8 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 20:29:08 +0900 Subject: [PATCH 05/22] =?UTF-8?q?if,match,eval=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 | 11 +- src/node.ts | 18 +- .../syntaxes/expression/control-flow.ts | 171 ++++++++++++++++-- src/parser/syntaxes/expression/index.ts | 152 +--------------- test/syntax.ts | 21 +++ 5 files changed, 204 insertions(+), 169 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 3e79323f..65aea078 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -155,6 +155,7 @@ declare namespace Ast { SubAssign, Assign, Expression, + ControlFlow, Plus, Minus, Not, @@ -213,6 +214,7 @@ type Attribute = NodeBase & { // @public (undocumented) type Block = NodeBase & { type: 'block'; + label?: string; statements: (Statement | Expression)[]; }; @@ -244,6 +246,9 @@ type Continue = NodeBase & { label?: string; }; +// @public (undocumented) +type ControlFlow = If | Match | Block | Each | For | Loop; + // @public (undocumented) type Definition = NodeBase & { type: 'def'; @@ -309,7 +314,7 @@ type Exists = NodeBase & { function expectAny(val: Value | null | undefined): asserts val is Value; // @public (undocumented) -type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; +type Expression = ControlFlow | Fn | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; // @public (undocumented) const FALSE: { @@ -380,6 +385,7 @@ type Identifier = NodeBase & { // @public (undocumented) type If = NodeBase & { type: 'if'; + label?: string; cond: Expression; then: Statement | Expression; elseif: { @@ -500,6 +506,7 @@ type Lteq = NodeBase & { // @public (undocumented) type Match = NodeBase & { type: 'match'; + label?: string; about: Expression; qs: { q: Expression; @@ -686,7 +693,7 @@ export class Scope { } // @public (undocumented) -type Statement = Definition | Return | Each | For | Loop | Break | Continue | Assign | AddAssign | SubAssign; +type Statement = Definition | Return | Break | Continue | Assign | AddAssign | SubAssign; // @public (undocumented) const STR: (str: VStr["value"]) => VStr; diff --git a/src/node.ts b/src/node.ts index c4a5d947..7d22fad7 100644 --- a/src/node.ts +++ b/src/node.ts @@ -123,13 +123,8 @@ export type Assign = NodeBase & { // expressions export type Expression = - Each | - For | - Loop | - If | + ControlFlow | Fn | - Match | - Block | Exists | Tmpl | Str | @@ -160,6 +155,14 @@ export type Expression = Index | Prop; +export type ControlFlow = + If | + Match | + Block | + Each | + For | + Loop; + const expressionTypes = [ 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'not', 'pow', 'mul', 'div', 'rem', 'add', 'sub', 'lt', 'lteq', 'gt', 'gteq', 'eq', 'neq', 'and', 'or', @@ -270,6 +273,7 @@ export type Or = NodeBase & { export type If = NodeBase & { type: 'if'; // if式 + label?: string; // ラベル cond: Expression; // 条件式 then: Statement | Expression; // then節 elseif: { @@ -293,6 +297,7 @@ export type Fn = NodeBase & { export type Match = NodeBase & { type: 'match'; // パターンマッチ + label?: string; // ラベル about: Expression; // 対象 qs: { q: Expression; // 条件 @@ -303,6 +308,7 @@ export type Match = NodeBase & { export type Block = NodeBase & { type: 'block'; // ブロックまたはeval式 + label?: string; // ラベル statements: (Statement | Expression)[]; // 処理 }; diff --git a/src/parser/syntaxes/expression/control-flow.ts b/src/parser/syntaxes/expression/control-flow.ts index 9dfc61aa..17d575f7 100644 --- a/src/parser/syntaxes/expression/control-flow.ts +++ b/src/parser/syntaxes/expression/control-flow.ts @@ -5,7 +5,7 @@ import { AiScriptSyntaxError } from '../../../error.js'; import { TokenKind } from '../../token.js'; import { unexpectedTokenError, NODE } from '../../utils.js'; -import { parseBlock, parseDest } from '../common.js'; +import { parseBlock, parseDest, parseOptionalSeparator } from '../common.js'; import { parseBlockOrStatement } from '../statements.js'; import { parseExpr } from './index.js'; @@ -14,28 +14,44 @@ import type { ITokenStream } from '../../streams/token-stream.js'; /** * ```abnf - * ExprWithLabel = "#" IDENT ":" Expression + * ControlFlowExpr = ["#" IDENT ":"] ControlFlowExprWithoutLabel * ``` */ -export function parseExprWithLabel(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop { - s.expect(TokenKind.Sharp); - s.next(); +export function parseControlFlowExpr(s: ITokenStream): Ast.ControlFlow { + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + s.next(); - s.expect(TokenKind.Identifier); - const label = s.getTokenValue(); - s.next(); + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); - s.expect(TokenKind.Colon); - s.next(); + s.expect(TokenKind.Colon); + s.next(); + } - const statement = parseControlFlowExpr(s); + const statement = parseControlFlowExprWithoutLabel(s); statement.label = label; return statement; } -export function parseControlFlowExpr(s: ITokenStream): Ast.Each | Ast.For | Ast.Loop { +/** + * ```abnf + * ControlFlowExprWithoutLabel = If / Match / Eval / Each / For / Loop + * ``` +*/ +export function parseControlFlowExprWithoutLabel(s: ITokenStream): Ast.ControlFlow { const tokenKind = s.getTokenKind(); switch (tokenKind) { + case TokenKind.IfKeyword: { + return parseIf(s); + } + case TokenKind.MatchKeyword: { + return parseMatch(s); + } + case TokenKind.EvalKeyword: { + return parseEval(s); + } case TokenKind.EachKeyword: { return parseEach(s); } @@ -51,10 +67,137 @@ export function parseControlFlowExpr(s: ITokenStream): Ast.Each | Ast.For | Ast. case TokenKind.WhileKeyword: { return parseWhile(s); } - default: { - throw unexpectedTokenError(tokenKind, s.getPos()); + } + throw unexpectedTokenError(tokenKind, s.getPos()); +} + +/** + * ```abnf + * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] + * ``` +*/ +function parseIf(s: ITokenStream): Ast.If { + const startPos = s.getPos(); + + s.expect(TokenKind.IfKeyword); + s.next(); + const cond = parseExpr(s, false); + const then = parseBlockOrStatement(s); + + if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + + const elseif: Ast.If['elseif'] = []; + while (s.is(TokenKind.ElifKeyword)) { + s.next(); + const elifCond = parseExpr(s, false); + const elifThen = parseBlockOrStatement(s); + if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + elseif.push({ cond: elifCond, then: elifThen }); + } + + let _else = undefined; + if (s.is(TokenKind.ElseKeyword)) { + s.next(); + _else = parseBlockOrStatement(s); + } + + return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); +} + +/** + * ```abnf + * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}" + * ``` +*/ +function parseMatch(s: ITokenStream): Ast.Match { + const startPos = s.getPos(); + + s.expect(TokenKind.MatchKeyword); + s.next(); + const about = parseExpr(s, false); + + s.expect(TokenKind.OpenBrace); + s.next(); + + if (s.is(TokenKind.NewLine)) { + s.next(); + } + + const qs: Ast.Match['qs'] = []; + let x: Ast.Match['default']; + if (s.is(TokenKind.CaseKeyword)) { + qs.push(parseMatchCase(s)); + let sep = parseOptionalSeparator(s); + while (s.is(TokenKind.CaseKeyword)) { + if (!sep) { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + qs.push(parseMatchCase(s)); + sep = parseOptionalSeparator(s); + } + if (s.is(TokenKind.DefaultKeyword)) { + if (!sep) { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + x = parseDefaultCase(s); + parseOptionalSeparator(s); } + } else if (s.is(TokenKind.DefaultKeyword)) { + x = parseDefaultCase(s); + parseOptionalSeparator(s); } + + s.expect(TokenKind.CloseBrace); + s.next(); + + return NODE('match', { about, qs, default: x }, startPos, s.getPos()); +} + +/** + * ```abnf + * MatchCase = "case" Expr "=>" BlockOrStatement + * ``` +*/ +function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { + s.expect(TokenKind.CaseKeyword); + s.next(); + const q = parseExpr(s, false); + s.expect(TokenKind.Arrow); + s.next(); + const a = parseBlockOrStatement(s); + return { q, a }; +} + +/** + * ```abnf + * DefaultCase = "default" "=>" BlockOrStatement + * ``` +*/ +function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { + s.expect(TokenKind.DefaultKeyword); + s.next(); + s.expect(TokenKind.Arrow); + s.next(); + return parseBlockOrStatement(s); +} + +/** + * ```abnf + * Eval = "eval" Block + * ``` +*/ +function parseEval(s: ITokenStream): Ast.Block { + const startPos = s.getPos(); + + s.expect(TokenKind.EvalKeyword); + s.next(); + const statements = parseBlock(s); + + return NODE('block', { statements }, startPos, s.getPos()); } /** diff --git a/src/parser/syntaxes/expression/index.ts b/src/parser/syntaxes/expression/index.ts index e510b647..fe96211a 100644 --- a/src/parser/syntaxes/expression/index.ts +++ b/src/parser/syntaxes/expression/index.ts @@ -2,9 +2,8 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../../error. 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 { parseBlockOrStatement } from '../statements.js'; -import { parseControlFlowExpr, parseExprWithLabel } from './control-flow.js'; +import { parseBlock, parseParams, parseType } from '../common.js'; +import { parseControlFlowExpr } from './control-flow.js'; import type * as Ast from '../../../node.js'; import type { ITokenStream } from '../../streams/token-stream.js'; @@ -194,25 +193,10 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { const startPos = s.getPos(); switch (s.getTokenKind()) { - case TokenKind.Sharp: { - return parseExprWithLabel(s); - } - case TokenKind.IfKeyword: { - if (isStatic) break; - return parseIf(s); - } case TokenKind.At: { if (isStatic) break; return parseFnExpr(s); } - case TokenKind.MatchKeyword: { - if (isStatic) break; - return parseMatch(s); - } - case TokenKind.EvalKeyword: { - if (isStatic) break; - return parseEval(s); - } case TokenKind.ExistsKeyword: { if (isStatic) break; return parseExists(s); @@ -292,6 +276,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { return expr; } } + if (isStatic) { + throw unexpectedTokenError(s.getTokenKind(), s.getPos()); + } return parseControlFlowExpr(s); } @@ -346,43 +333,6 @@ function parseCall(s: ITokenStream, target: Ast.Expression): Ast.Call { }, startPos, s.getPos()); } -/** - * ```abnf - * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] - * ``` -*/ -function parseIf(s: ITokenStream): Ast.If { - const startPos = s.getPos(); - - s.expect(TokenKind.IfKeyword); - s.next(); - const cond = parseExpr(s, false); - const then = parseBlockOrStatement(s); - - if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { - s.next(); - } - - const elseif: Ast.If['elseif'] = []; - while (s.is(TokenKind.ElifKeyword)) { - s.next(); - const elifCond = parseExpr(s, false); - const elifThen = parseBlockOrStatement(s); - if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { - s.next(); - } - elseif.push({ cond: elifCond, then: elifThen }); - } - - let _else = undefined; - if (s.is(TokenKind.ElseKeyword)) { - s.next(); - _else = parseBlockOrStatement(s); - } - - return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); -} - /** * ```abnf * FnExpr = "@" Params [":" Type] Block @@ -407,98 +357,6 @@ function parseFnExpr(s: ITokenStream): Ast.Fn { return NODE('fn', { params: params, retType: type, children: body }, startPos, s.getPos()); } -/** - * ```abnf - * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}" - * ``` -*/ -function parseMatch(s: ITokenStream): Ast.Match { - const startPos = s.getPos(); - - s.expect(TokenKind.MatchKeyword); - s.next(); - const about = parseExpr(s, false); - - s.expect(TokenKind.OpenBrace); - s.next(); - - if (s.is(TokenKind.NewLine)) { - s.next(); - } - - const qs: Ast.Match['qs'] = []; - let x: Ast.Match['default']; - if (s.is(TokenKind.CaseKeyword)) { - qs.push(parseMatchCase(s)); - let sep = parseOptionalSeparator(s); - while (s.is(TokenKind.CaseKeyword)) { - if (!sep) { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - qs.push(parseMatchCase(s)); - sep = parseOptionalSeparator(s); - } - if (s.is(TokenKind.DefaultKeyword)) { - if (!sep) { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - x = parseDefaultCase(s); - parseOptionalSeparator(s); - } - } else if (s.is(TokenKind.DefaultKeyword)) { - x = parseDefaultCase(s); - parseOptionalSeparator(s); - } - - s.expect(TokenKind.CloseBrace); - s.next(); - - return NODE('match', { about, qs, default: x }, startPos, s.getPos()); -} - -/** - * ```abnf - * MatchCase = "case" Expr "=>" BlockOrStatement - * ``` -*/ -function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { - s.expect(TokenKind.CaseKeyword); - s.next(); - const q = parseExpr(s, false); - s.expect(TokenKind.Arrow); - s.next(); - const a = parseBlockOrStatement(s); - return { q, a }; -} - -/** - * ```abnf - * DefaultCase = "default" "=>" BlockOrStatement - * ``` -*/ -function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { - s.expect(TokenKind.DefaultKeyword); - s.next(); - s.expect(TokenKind.Arrow); - s.next(); - return parseBlockOrStatement(s); -} - -/** - * ```abnf - * Eval = "eval" Block - * ``` -*/ -function parseEval(s: ITokenStream): Ast.Block { - const startPos = s.getPos(); - - s.expect(TokenKind.EvalKeyword); - s.next(); - const statements = parseBlock(s); - - return NODE('block', { statements }, startPos, s.getPos()); -} - /** * ```abnf * Exists = "exists" Reference diff --git a/test/syntax.ts b/test/syntax.ts index da16368a..63db80d8 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -1641,6 +1641,13 @@ describe('if', () => { `); }); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: if true { 1 } + `); + eq(res, NUM(1)); + }); }); describe('eval', () => { @@ -1656,6 +1663,13 @@ describe('eval', () => { `); eq(res, NUM(3)); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: eval { 1 } + `); + eq(res, NUM(1)); + }); }); describe('match', () => { @@ -1737,6 +1751,13 @@ describe('match', () => { `); }); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: match 0 { default => 1 } + `); + eq(res, NUM(1)); + }); }); describe('exists', () => { From 2f442c02bdb56244561f5da07648b493232b65c3 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 18 Nov 2024 22:19:27 +0900 Subject: [PATCH 06/22] =?UTF-8?q?if,match,eval=E3=82=92break=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/control.ts | 16 ++++++-- src/interpreter/index.ts | 22 +++++------ .../plugins/validate-jump-statements.ts | 10 ++++- test/jump-statements.ts | 38 +++++++++++++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 3bbba93e..19528c3e 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -1,6 +1,6 @@ import { AiScriptRuntimeError } from '../error.js'; +import { NULL, type Value } from './value.js'; import type { Reference } from './reference.js'; -import type { Value } from './value.js'; export type CReturn = { type: 'return'; @@ -10,7 +10,7 @@ export type CReturn = { export type CBreak = { type: 'break'; label?: string; - value: null; + value: Value; }; export type CContinue = { @@ -30,7 +30,7 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ export const BREAK = (label?: string): CBreak => ({ type: 'break' as const, label, - value: null, + value: NULL, }); export const CONTINUE = (label?: string): CContinue => ({ @@ -50,6 +50,16 @@ export function unWrapRet(v: Value | Control): Value { } } +/** + * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 + */ +export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && v.label != null && v.label === label) { + return v.value; + } + return v; +} + export function assertValue(v: Value | Control): asserts v is Value { switch (v.type) { case 'return': diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index a4669c86..a1b1e4df 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/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index 3ca50959..99f8fae0 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -15,6 +15,14 @@ function isInValidLoopScope(ancestors: Ast.Node[], label?: string): boolean { } return true; } + case 'if': + case 'match': + case 'block': { + if (label == null || label !== ancestor.label) { + continue; + } + return true; + } case 'fn': return false; } @@ -35,7 +43,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 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; } diff --git a/test/jump-statements.ts b/test/jump-statements.ts index a7897ffc..6877a3e2 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 a361f01e067cf338d00832e1bbe394da9e2aa2f7 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 16:52:02 +0900 Subject: [PATCH 07/22] =?UTF-8?q?return,break,continue=E3=82=92=E5=BC=8F?= =?UTF-8?q?=E6=89=B1=E3=81=84=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 4 +- src/node.ts | 6 +- src/parser/syntaxes/expression/index.ts | 73 +++++++++++ test/jump-statements.ts | 166 ++++++++++++++---------- 4 files changed, 173 insertions(+), 76 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 65aea078..75fb9c47 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -314,7 +314,7 @@ type Exists = NodeBase & { function expectAny(val: Value | null | undefined): asserts val is Value; // @public (undocumented) -type Expression = ControlFlow | Fn | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; +type Expression = ControlFlow | Return | Break | Continue | Fn | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; // @public (undocumented) const FALSE: { @@ -693,7 +693,7 @@ export class Scope { } // @public (undocumented) -type Statement = Definition | Return | Break | Continue | Assign | AddAssign | SubAssign; +type Statement = Definition | Assign | AddAssign | SubAssign; // @public (undocumented) const STR: (str: VStr["value"]) => VStr; diff --git a/src/node.ts b/src/node.ts index 7d22fad7..4fae364b 100644 --- a/src/node.ts +++ b/src/node.ts @@ -34,9 +34,6 @@ export type Meta = NodeBase & { export type Statement = Definition | - Return | - Break | - Continue | Assign | AddAssign | SubAssign; @@ -124,6 +121,9 @@ export type Assign = NodeBase & { export type Expression = ControlFlow | + Return | + Break | + Continue | Fn | Exists | Tmpl | diff --git a/src/parser/syntaxes/expression/index.ts b/src/parser/syntaxes/expression/index.ts index fe96211a..93bc5eab 100644 --- a/src/parser/syntaxes/expression/index.ts +++ b/src/parser/syntaxes/expression/index.ts @@ -275,6 +275,18 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { s.next(); return expr; } + case TokenKind.ReturnKeyword: { + if (isStatic) break; + return parseReturn(s); + } + case TokenKind.BreakKeyword: { + if (isStatic) break; + return parseBreak(s); + } + case TokenKind.ContinueKeyword: { + if (isStatic) break; + return parseContinue(s); + } } if (isStatic) { throw unexpectedTokenError(s.getTokenKind(), s.getPos()); @@ -505,6 +517,67 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Arr { return NODE('arr', { value }, startPos, s.getPos()); } +/** + * ```abnf + * Return = "return" Expr + * ``` +*/ +function parseReturn(s: ITokenStream): Ast.Return { + const startPos = s.getPos(); + + s.expect(TokenKind.ReturnKeyword); + s.next(); + const expr = parseExpr(s, false); + + return NODE('return', { expr }, 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()); +} + //#region Pratt parsing type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 6877a3e2..cf56e651 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -34,12 +34,12 @@ describe('return', () => { test.concurrent('cond', async () => { const res = await exe(` @f() { - let a = if eval { return true } {} + let a = if (return true) {} } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if eval { return true } {}')); + assert.rejects(() => exe('<: if (return true) {}')); }); test.concurrent('then', async () => { @@ -58,12 +58,12 @@ describe('return', () => { test.concurrent('elif cond', async () => { const res = await exe(` @f() { - let a = if false {} elif eval { return true } {} + let a = if false {} elif (return true) {} } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); + assert.rejects(() => exe('<: if false {} elif (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 }')); + assert.rejects(() => exe('<: if false {} elif true (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 }')); + assert.rejects(() => exe('<: if false {} else (return true)')); }); }); @@ -99,19 +99,19 @@ describe('return', () => { test.concurrent('about', async () => { const res = await exe(` @f() { - let a = match eval { return 1 } {} + let a = match (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: match eval { return 1 } {}')); + assert.rejects(() => exe('<: match (return 1) {}')); }); test.concurrent('case q', async () => { const res = await exe(` @f() { let a = match 0 { - case eval { return 0 } => { + case (return 0) => { return 1 } } @@ -119,7 +119,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) + assert.rejects(() => exe('<: match 0 { case (return 0) => {} }')) }); test.concurrent('case a', async () => { @@ -157,23 +157,23 @@ describe('return', () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } + 2 + (return 1) + 2 } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 } + 2')); + assert.rejects(() => exe('<: (return 1) + 2')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - 1 + eval { return 2 } + 1 + (return 2) } <: f() `); eq(res, NUM(2)); - assert.rejects(() => exe('<: 1 + eval { return 2 }')); + assert.rejects(() => exe('<: 1 + (return 2)')); }); }); @@ -181,23 +181,23 @@ describe('return', () => { test.concurrent('callee', async () => { const res = await exe(` @f() { - eval { return print }('Hello, world!') + (return print)('Hello, world!') } f()('Hi') `); eq(res, STR('Hi')); - assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); + assert.rejects(() => exe(`(return print)('Hello, world!')`)); }); test.concurrent('arg', async () => { const res = await exe(` @f() { - print(eval { return 'Hello, world!' }) + print(return 'Hello, world!') } <: f() `); eq(res, STR('Hello, world!')); - assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) + assert.rejects(() => exe(`print(return 'Hello, world')`)) }); }); @@ -205,34 +205,34 @@ describe('return', () => { test.concurrent('times', async () => { const res = await exe(` @f() { - for eval { return 1 } {} + for (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for eval { return 1 } {}')); + assert.rejects(() => exe('for (return 1) {}')); }); test.concurrent('from', async () => { const res = await exe(` @f() { - for let i = eval { return 1 }, 2 {} + for let i = (return 1), 2 {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); + assert.rejects(() => exe('for let i = (return 1), 2 {}')); }); test.concurrent('to', async () => { const res = await exe(` @f() { - for let i = 0, eval { return 1 } {} + for let i = 0, (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); + assert.rejects(() => exe('for let i = 0, (return 1) {}')); }); test.concurrent('for', async () => { @@ -253,12 +253,12 @@ describe('return', () => { test.concurrent('items', async () => { const res = await exe(` @f() { - each let v, [eval { return 1 }] {} + each let v, [return 1] {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); + assert.rejects(() => exe('each let v, [return 1] {}')); }); test.concurrent('for', async () => { @@ -280,72 +280,72 @@ describe('return', () => { const res = await exe(` @f() { let a = null - a = eval { return 1 } + a = (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = null; a = eval { return 1 }')); + assert.rejects(() => exe('let a = null; a = (return 1)')); }); test.concurrent('index target', async () => { const res = await exe(` @f() { let a = [null] - eval { return a }[0] = 1 + (return a)[0] = 1 } <: f() `); eq(res, ARR([NULL])); - assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); + assert.rejects(() => exe('let a = [null]; (return a)[0] = 1')); }); test.concurrent('index', async () => { const res = await exe(` @f() { let a = [null] - a[eval { return 0 }] = 1 + a[return 0] = 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); + assert.rejects(() => exe('let a = [null]; a[return 0] = 1')); }); test.concurrent('prop target', async () => { const res = await exe(` @f() { let o = {} - eval { return o }.p = 1 + (return o).p = 1 } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); + assert.rejects(() => exe('let o = {}; (return o).p = 1')); }); test.concurrent('arr', async () => { const res = await exe(` @f() { let o = {} - [eval { return o }.p] = [1] + [(return o).p] = [1] } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); + assert.rejects(() => exe('let o = {}; [(return o).p] = [1]')); }); test.concurrent('obj', async () => { const res = await exe(` @f() { let o = {} - { a: eval { return o }.p } = { a: 1 } + { a: (return o).p } = { a: 1 } } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); + assert.rejects(() => exe('let o = {}; { a: (return o).p } = { a: 1 }')); }); }); @@ -354,24 +354,24 @@ describe('return', () => { const res = await exe(` @f() { let a = [0] - a[eval { return 0 }] += 1 + a[return 0] += 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); + assert.rejects(() => exe('let a = [0]; a[return 0] += 1')); }); test.concurrent('expr', async () => { const res = await exe(` @f() { let a = 0 - a += eval { return 1 } + a += (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); + assert.rejects(() => exe('let a = 0; a += (return 1)')); }); }); @@ -380,114 +380,114 @@ describe('return', () => { const res = await exe(` @f() { let a = [0] - a[eval { return 0 }] -= 1 + a[return 0] -= 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); + assert.rejects(() => exe('let a = [0]; a[return 0] -= 1')); }); test.concurrent('expr', async () => { const res = await exe(` @f() { let a = 0 - a -= eval { return 1 } + a -= (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); + assert.rejects(() => exe('let a = 0; a -= (return 1)')); }); }); test.concurrent('in array', async () => { const res = await exe(` @f() { - let a = [eval { return 1 }] + let a = [return 1] } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }]')); + assert.rejects(() => exe('<: [return 1]')); }); test.concurrent('in object', async () => { const res = await exe(` @f() { let o = { - p: eval { return 1 } + p: (return 1) } } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }')); + assert.rejects(() => exe('<: { p: (return 1) }')); }); test.concurrent('in prop', async () => { const res = await exe(` @f() { let p = { - p: eval { return 1 } + p: (return 1) }.p } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); + assert.rejects(() => exe('<: { p: (return 1) }.p')); }); describe('in index', () => { test.concurrent('target', async () => { const res = await exe(` @f() { - let v = [eval { return 1 }][0] + let v = [return 1][0] } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }][0]')); + assert.rejects(() => exe('<: [return 1][0]')); }); test.concurrent('index', async () => { const res = await exe(` @f() { - let v = [1][eval { return 0 }] + let v = [1][return 0] } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: [0][eval { return 1 }]')); + assert.rejects(() => exe('<: [0][return 1]')); }); }); test.concurrent('in not', async () => { const res = await exe(` @f() { - let b = !eval { return true } + let b = !(return true) } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: !eval { return true }')); + assert.rejects(() => exe('<: !(return true)')); }); test.concurrent('in function default param', async () => { const res = await exe(` @f() { - let g = @(x = eval { return 1 }) {} + let g = @(x = (return 1)) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); + assert.rejects(() => exe('<: @(x = (return 1)){}')); }); test.concurrent('in template', async () => { const res = await exe(` @f() { - let s = \`{eval { return 1 }}\` + let s = \`{(return 1)}\` } <: f() `); @@ -498,35 +498,35 @@ describe('return', () => { test.concurrent('in return', async () => { const res = await exe(` @f() { - return eval { return 1 } + 2 + return (return 1) + 2 } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('return eval { return 1 } + 2')); + assert.rejects(() => exe('return (return 1) + 2')); }); describe('in and', async () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } && false + (return 1) && false } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } && false')); + assert.rejects(() => exe('(return 1) && false')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - true && eval { return 1 } + true && (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('true && eval { return 1 }')); + assert.rejects(() => exe('true && (return 1)')); }); }); @@ -534,23 +534,23 @@ describe('return', () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } || false + (return 1) || false } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } || false')); + assert.rejects(() => exe('(return 1) || false')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - false || eval { return 1 } + false || (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('false || eval { return 1 }')); + assert.rejects(() => exe('false || (return 1)')); }); }); }); @@ -625,6 +625,18 @@ describe('break', () => { `)); }); + test.concurrent('as expr', async () => { + const res = await exe(` + var x = true + for 1 { + x = false || break + } + <: x + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: false || break')); + }); + test.concurrent('invalid label', async () => { assert.rejects(() => exe(` for 1 { @@ -1127,6 +1139,18 @@ describe('continue', () => { `)); }); + test.concurrent('as expr', async () => { + const res = await exe(` + var x = true + for 1 { + x = false || continue + } + <: x + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: false || continue')); + }); + test.concurrent('invalid label', async () => { assert.rejects(() => exe(` for 1 { From 3168e6cb144847aaf6869a4eb143ccd25c40f686 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 17:20:33 +0900 Subject: [PATCH 08/22] =?UTF-8?q?if,each,for,do-while,while=E3=81=AF?= =?UTF-8?q?=E6=B3=A2=E6=8B=AC=E5=BC=A7=E5=BF=85=E9=A0=88=E3=80=81match?= =?UTF-8?q?=E3=81=AF=E5=BC=8F=E3=82=92=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 14 ++-- src/node.ts | 14 ++-- .../syntaxes/expression/control-flow.ts | 70 ++++++++++++------- src/parser/syntaxes/statements.ts | 15 ---- test/index.ts | 22 ++---- test/interpreter.ts | 4 +- test/primitive-props.ts | 4 +- test/syntax.ts | 38 +++------- 8 files changed, 79 insertions(+), 102 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 75fb9c47..ea34f7a1 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -272,7 +272,7 @@ type Each = NodeBase & { label?: string; var: Expression; items: Expression; - for: Statement | Expression; + for: Block; }; // @public (undocumented) @@ -356,7 +356,7 @@ type For = NodeBase & { from?: Expression; to?: Expression; times?: Expression; - for: Statement | Expression; + for: Block; }; // @public (undocumented) @@ -387,12 +387,12 @@ type If = NodeBase & { type: 'if'; label?: string; cond: Expression; - then: Statement | Expression; + then: Block; elseif: { cond: Expression; - then: Statement | Expression; + then: Block; }[]; - else?: Statement | Expression; + else?: Block; }; // @public (undocumented) @@ -510,9 +510,9 @@ type Match = NodeBase & { about: Expression; qs: { q: Expression; - a: Statement | Expression; + a: Expression; }[]; - default?: Statement | Expression; + default?: Expression; }; // @public (undocumented) diff --git a/src/node.ts b/src/node.ts index 4fae364b..bf080404 100644 --- a/src/node.ts +++ b/src/node.ts @@ -70,7 +70,7 @@ export type Each = NodeBase & { label?: string; // ラベル var: Expression; // イテレータ宣言 items: Expression; // 配列 - for: Statement | Expression; // 本体処理 + for: Block; // 本体処理 }; export type For = NodeBase & { @@ -80,7 +80,7 @@ export type For = NodeBase & { from?: Expression; // 開始値 to?: Expression; // 終値 times?: Expression; // 回数 - for: Statement | Expression; // 本体処理 + for: Block; // 本体処理 }; export type Loop = NodeBase & { @@ -275,12 +275,12 @@ export type If = NodeBase & { type: 'if'; // if式 label?: string; // ラベル cond: Expression; // 条件式 - then: Statement | Expression; // then節 + then: Block; // then節 elseif: { cond: Expression; // elifの条件式 - then: Statement | Expression;// elif節 + then: Block;// elif節 }[]; - else?: Statement | Expression; // else節 + else?: Block; // else節 }; export type Fn = NodeBase & { @@ -301,9 +301,9 @@ export type Match = NodeBase & { about: Expression; // 対象 qs: { q: Expression; // 条件 - a: Statement | Expression; // 結果 + a: Expression; // 結果 }[]; - default?: Statement | Expression; // デフォルト値 + default?: Expression; // デフォルト値 }; export type Block = NodeBase & { diff --git a/src/parser/syntaxes/expression/control-flow.ts b/src/parser/syntaxes/expression/control-flow.ts index 17d575f7..c9d31cfa 100644 --- a/src/parser/syntaxes/expression/control-flow.ts +++ b/src/parser/syntaxes/expression/control-flow.ts @@ -6,7 +6,6 @@ import { AiScriptSyntaxError } from '../../../error.js'; import { TokenKind } from '../../token.js'; import { unexpectedTokenError, NODE } from '../../utils.js'; import { parseBlock, parseDest, parseOptionalSeparator } from '../common.js'; -import { parseBlockOrStatement } from '../statements.js'; import { parseExpr } from './index.js'; import type * as Ast from '../../../node.js'; @@ -73,7 +72,7 @@ export function parseControlFlowExprWithoutLabel(s: ITokenStream): Ast.ControlFl /** * ```abnf - * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] + * If = "if" Expr Block *("elif" Expr Block) ["else" Block] * ``` */ function parseIf(s: ITokenStream): Ast.If { @@ -82,7 +81,7 @@ function parseIf(s: ITokenStream): Ast.If { s.expect(TokenKind.IfKeyword); s.next(); const cond = parseExpr(s, false); - const then = parseBlockOrStatement(s); + const then = parseBlockAsExpr(s); if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { s.next(); @@ -92,7 +91,7 @@ function parseIf(s: ITokenStream): Ast.If { while (s.is(TokenKind.ElifKeyword)) { s.next(); const elifCond = parseExpr(s, false); - const elifThen = parseBlockOrStatement(s); + const elifThen = parseBlockAsExpr(s); if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { s.next(); } @@ -102,7 +101,7 @@ function parseIf(s: ITokenStream): Ast.If { let _else = undefined; if (s.is(TokenKind.ElseKeyword)) { s.next(); - _else = parseBlockOrStatement(s); + _else = parseBlockAsExpr(s); } return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); @@ -159,7 +158,7 @@ function parseMatch(s: ITokenStream): Ast.Match { /** * ```abnf - * MatchCase = "case" Expr "=>" BlockOrStatement + * MatchCase = "case" Expr "=>" Block * ``` */ function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { @@ -168,13 +167,13 @@ function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { const q = parseExpr(s, false); s.expect(TokenKind.Arrow); s.next(); - const a = parseBlockOrStatement(s); + const a = parseBlockOrExpr(s); return { q, a }; } /** * ```abnf - * DefaultCase = "default" "=>" BlockOrStatement + * DefaultCase = "default" "=>" Block * ``` */ function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { @@ -182,7 +181,7 @@ function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { s.next(); s.expect(TokenKind.Arrow); s.next(); - return parseBlockOrStatement(s); + return parseBlockOrExpr(s); } /** @@ -202,8 +201,8 @@ function parseEval(s: ITokenStream): Ast.Block { /** * ```abnf - * Each = "each" "(" "let" Dest "," Expr ")" BlockOrStatement - * / "each" "let" Dest "," Expr BlockOrStatement + * Each = "each" "(" "let" Dest "," Expr ")" Block + * / "each" "let" Dest "," Expr Block * ``` */ function parseEach(s: ITokenStream): Ast.Each { @@ -236,7 +235,7 @@ function parseEach(s: ITokenStream): Ast.Each { s.next(); } - const body = parseBlockOrStatement(s); + const body = parseBlockAsExpr(s); return NODE('each', { var: dest, @@ -248,10 +247,10 @@ function parseEach(s: ITokenStream): Ast.Each { /** * ```abnf * For = ForRange / ForTimes - * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" BlockOrStatement - * / "for" "let" IDENT ["=" Expr] "," Expr BlockOrStatement - * ForTimes = "for" "(" Expr ")" BlockOrStatement - * / "for" Expr BlockOrStatement + * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" Block + * / "for" "let" IDENT ["=" Expr] "," Expr Block + * ForTimes = "for" "(" Expr ")" Block + * / "for" Expr Block * ``` */ function parseFor(s: ITokenStream): Ast.For { @@ -297,7 +296,7 @@ function parseFor(s: ITokenStream): Ast.For { s.next(); } - const body = parseBlockOrStatement(s); + const body = parseBlockAsExpr(s); return NODE('for', { var: name, @@ -315,7 +314,7 @@ function parseFor(s: ITokenStream): Ast.For { s.next(); } - const body = parseBlockOrStatement(s); + const body = parseBlockAsExpr(s); return NODE('for', { times, @@ -341,14 +340,14 @@ function parseLoop(s: ITokenStream): Ast.Loop { /** * ```abnf - * Loop = "do" BlockOrStatement "while" Expr + * Loop = "do" Block "while" Expr * ``` */ function parseDoWhile(s: ITokenStream): Ast.Loop { const doStartPos = s.getPos(); s.expect(TokenKind.DoKeyword); s.next(); - const body = parseBlockOrStatement(s); + const body = parseBlockAsExpr(s); const whilePos = s.getPos(); s.expect(TokenKind.WhileKeyword); s.next(); @@ -360,7 +359,9 @@ function parseDoWhile(s: ITokenStream): Ast.Loop { body, NODE('if', { cond: NODE('not', { expr: cond }, whilePos, endPos), - then: NODE('break', {}, endPos, endPos), + then: NODE('block', { + statements: [NODE('break', {}, endPos, endPos)], + }, endPos, endPos), elseif: [], }, whilePos, endPos), ], @@ -369,7 +370,7 @@ function parseDoWhile(s: ITokenStream): Ast.Loop { /** * ```abnf - * Loop = "while" Expr BlockOrStatement + * Loop = "while" Expr Block * ``` */ function parseWhile(s: ITokenStream): Ast.Loop { @@ -378,16 +379,37 @@ function parseWhile(s: ITokenStream): Ast.Loop { s.next(); const cond = parseExpr(s, false); const condEndPos = s.getPos(); - const body = parseBlockOrStatement(s); + const body = parseBlockAsExpr(s); return NODE('loop', { statements: [ NODE('if', { cond: NODE('not', { expr: cond }, startPos, condEndPos), - then: NODE('break', {}, condEndPos, condEndPos), + then: NODE('block', { + statements: [NODE('break', {}, condEndPos, condEndPos)], + }, condEndPos, condEndPos), elseif: [], }, startPos, condEndPos), body, ], }, startPos, s.getPos()); } + +/** + * ```abnf + * BlockOrExpr = Block / Expr + * ``` +*/ +function parseBlockOrExpr(s: ITokenStream): Ast.Expression { + if (s.is(TokenKind.OpenBrace)) { + return parseBlockAsExpr(s); + } else { + return parseExpr(s, false); + } +} + +function parseBlockAsExpr(s: ITokenStream): Ast.Block { + const startPos = s.getPos(); + const statements = parseBlock(s); + return NODE('block', { statements }, startPos, s.getPos()); +} diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index dc98be6d..4a5a103a 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -64,21 +64,6 @@ export function parseDefStatement(s: ITokenStream): Ast.Definition { } } -/** - * ```abnf - * BlockOrStatement = Block / Statement - * ``` -*/ -export function parseBlockOrStatement(s: ITokenStream): Ast.Statement | Ast.Expression { - if (s.is(TokenKind.OpenBrace)) { - const startPos = s.getPos(); - const statements = parseBlock(s); - return NODE('block', { statements }, startPos, s.getPos()); - } else { - return parseStatement(s); - } -} - /** * ```abnf * VarDef = ("let" / "var") Dest [":" Type] "=" Expr diff --git a/test/index.ts b/test/index.ts index af97bf11..5925e9fd 100644 --- a/test/index.ts +++ b/test/index.ts @@ -690,18 +690,6 @@ describe('Return', () => { eq(res, STR('kawaii')); }); - test.concurrent('Early return without block', async () => { - const res = await exe(` - @f() { - if true return "ai" - - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); - test.concurrent('return inside for', async () => { const res = await exe(` @f() { @@ -808,7 +796,7 @@ describe('type declaration', () => { x.push(0) y = "abc" var r: bool = z(x[0]) - x.push(if r 5 else 10) + x.push(if r { 5 } else { 10 }) x } @@ -1100,10 +1088,10 @@ describe('extra', () => { let res = [] for (let i = 1, 15) { let msg = - if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i + if i % 15 == 0 { "FizzBuzz" } + elif i % 3 == 0 { "Fizz" } + elif i % 5 == 0 { "Buzz" } + else { i } res.push(msg) } <: res diff --git a/test/interpreter.ts b/test/interpreter.ts index bcf9e64d..49aeb912 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -103,9 +103,9 @@ describe('error location', () => { test.concurrent('Error in passed function', async () => { return expect(exeAndGetErrPos(`// (の位置 [1, 2, 3].map(@(v){ - if v==1 Core:abort("error") + if v==1 { Core:abort("error") } }) - `)).resolves.toEqual({ line: 3, column: 23}); + `)).resolves.toEqual({ line: 3, column: 25 }); }); test.concurrent('No such prop', async () => { diff --git a/test/primitive-props.ts b/test/primitive-props.ts index 6ea2cdab..badeff65 100644 --- a/test/primitive-props.ts +++ b/test/primitive-props.ts @@ -89,7 +89,7 @@ describe('str', () => { str.index_of('3', -2) == 8, str.index_of('3', -7) == 3, str.index_of('3', 10) == -1, - ].map(@(v){if (v) '1' else '0'}).join() + ].map(@(v){if v { '1' } else { '0' }}).join() `); eq(res, STR('11111111')); }); @@ -485,7 +485,7 @@ describe('arr', () => { arr.index_of(3, -2) == 8, arr.index_of(3, -7) == 3, arr.index_of(3, 10) == -1, - ].map(@(v){if (v) '1' else '0'}).join() + ].map(@(v){if v { '1' } else { '0' }}).join() `); eq(res, STR('11111111')); }); diff --git a/test/syntax.ts b/test/syntax.ts index 63db80d8..48623df2 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -723,7 +723,7 @@ describe('for', () => { const res = await exe(` var count = 0 for (let i, 20) { - if (i == 11) break + if i == 11 { break } count += i } <: count @@ -735,7 +735,7 @@ describe('for', () => { const res = await exe(` var count = 0 for (let i, 10) { - if (i == 5) continue + if i == 5 { continue } count = (count + 1) } <: count @@ -743,15 +743,6 @@ describe('for', () => { eq(res, NUM(9)); }); - test.concurrent('single statement', async () => { - const res = await exe(` - var count = 0 - for 10 count += 1 - <: count - `); - eq(res, NUM(10)); - }); - test.concurrent('var name without space', async () => { try { await exe(` @@ -819,7 +810,7 @@ describe('each', () => { const res = await exe(` let msgs = [] each let item, ["ai", "chan", "kawaii", "yo"] { - if (item == "kawaii") break + if item == "kawaii" { break } msgs.push([item, "!"].join()) } <: msgs @@ -827,15 +818,6 @@ describe('each', () => { eq(res, ARR([STR('ai!'), STR('chan!')])); }); - test.concurrent('single statement', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); - }); - test.concurrent('var name without space', async () => { try { await exe(` @@ -954,7 +936,7 @@ describe('loop', () => { const res = await exe(` var count = 0 loop { - if (count == 10) break + if count == 10 { break } count = (count + 1) } <: count @@ -968,8 +950,8 @@ describe('loop', () => { var b = [] loop { var x = a.shift() - if (x == "chan") continue - if (x == "yo") break + if x == "chan" { continue } + if x == "yo" { break } b.push(x) } <: b @@ -981,7 +963,7 @@ describe('loop', () => { const res = await exe(` var count = 0 #label: loop { - if (count == 10) break + if count == 10 { break } count = (count + 1) } <: count @@ -1460,7 +1442,7 @@ describe('Infix expression', () => { }); test.concurrent('number + if expression', async () => { - eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); + eq(await exe('<: 1 + if true { 1 } else { 2 }'), NUM(2)); }); test.concurrent('number + match expression', async () => { @@ -1611,12 +1593,12 @@ describe('if', () => { test.concurrent('expr', async () => { const res1 = await exe(` - <: if true "ai" else "kawaii" + <: if true { "ai" } else { "kawaii" } `); eq(res1, STR('ai')); const res2 = await exe(` - <: if false "ai" else "kawaii" + <: if false { "ai" } else { "kawaii" } `); eq(res2, STR('kawaii')); }); From adde75002f8564526a79f18fd0dfe83c6cdcc5e1 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 20:51:06 +0900 Subject: [PATCH 09/22] =?UTF-8?q?break=E5=BC=8F=E3=81=A7=E3=83=96=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=AE=E5=80=A4=E3=82=92=E8=BF=94=E3=81=9B?= =?UTF-8?q?=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 --- etc/aiscript.api.md | 1 + src/interpreter/control.ts | 14 ++- src/interpreter/index.ts | 52 +++++----- src/node.ts | 1 + .../syntaxes/expression/control-flow.ts | 2 +- src/parser/syntaxes/expression/index.ts | 16 +++- src/parser/syntaxes/statements.ts | 70 -------------- test/jump-statements.ts | 94 +++++++++++++++++++ 8 files changed, 144 insertions(+), 106 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index ea34f7a1..a1a884cb 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -231,6 +231,7 @@ type Bool = NodeBase & { type Break = NodeBase & { type: 'break'; label?: string; + expr?: Expression; }; // @public (undocumented) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 19528c3e..4eb520b7 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -27,10 +27,10 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ value: v, }); -export const BREAK = (label?: string): CBreak => ({ +export const BREAK = (v: CBreak['value'], label?: string): CBreak => ({ type: 'break' as const, label, - value: NULL, + value: v, }); export const CONTINUE = (label?: string): CContinue => ({ @@ -50,6 +50,16 @@ export function unWrapRet(v: Value | Control): Value { } } +/** + * 値が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; + } + return v; +} + /** * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 */ diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index a1b1e4df..771b594c 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -4,15 +4,15 @@ import { autobind } from '../utils/mini-autobind.js'; import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError, AiScriptHostsideError } from '../error.js'; -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, unWrapLabeledBreak } from './control.js'; +import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control, unWrapLabeledBreak, unWrapBreak } 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'; import { Variable } from './variable.js'; import { Reference } from './reference.js'; +import type * as Ast from '../node.js'; import type { JsValue } from './util.js'; import type { Value, VFn, VUserFn } from './value.js'; @@ -292,11 +292,6 @@ export class Interpreter { } } - @autobind - private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { - return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope, callStack); - } - @autobind private async _evalBinaryOperation(op: string, leftExpr: Ast.Expression, rightExpr: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { const callee = scope.get(op); @@ -371,7 +366,7 @@ export class Interpreter { } assertBoolean(cond); if (cond.value) { - return unWrapLabeledBreak(await this._evalClause(node.then, scope, callStack), node.label); + return unWrapLabeledBreak(await this._eval(node.then, scope, callStack), node.label); } for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); @@ -380,11 +375,11 @@ export class Interpreter { } assertBoolean(cond); if (cond.value) { - return unWrapLabeledBreak(await this._evalClause(elseif.then, scope, callStack), node.label); + return unWrapLabeledBreak(await this._eval(elseif.then, scope, callStack), node.label); } } if (node.else) { - return unWrapLabeledBreak(await this._evalClause(node.else, scope, callStack), node.label); + return unWrapLabeledBreak(await this._eval(node.else, scope, callStack), node.label); } return NULL; } @@ -400,11 +395,11 @@ export class Interpreter { return unWrapLabeledBreak(q, node.label); } if (eq(about, q)) { - return unWrapLabeledBreak(await this._evalClause(qa.a, scope, callStack), node.label); + return unWrapLabeledBreak(await this._eval(qa.a, scope, callStack), node.label); } } if (node.default) { - return unWrapLabeledBreak(await this._evalClause(node.default, scope, callStack), node.label); + return unWrapLabeledBreak(await this._eval(node.default, scope, callStack), node.label); } return NULL; } @@ -414,10 +409,7 @@ 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; + return unWrapBreak(v, node.label); } else if (v.type === 'continue') { if (v.label != null && v.label !== node.label) { return v; @@ -426,7 +418,6 @@ export class Interpreter { return v; } } - return NULL; } case 'for': { @@ -437,12 +428,9 @@ export class Interpreter { } assertNumber(times); for (let i = 0; i < times.value; i++) { - const v = await this._evalClause(node.for, scope, callStack); + const v = await this._eval(node.for, scope, callStack); if (v.type === 'break') { - if (v.label != null && v.label !== node.label) { - return v; - } - break; + return unWrapBreak(v, node.label); } else if (v.type === 'continue') { if (v.label != null && v.label !== node.label) { return v; @@ -470,10 +458,7 @@ export class Interpreter { }], ])), callStack); if (v.type === 'break') { - if (v.label != null && v.label !== node.label) { - return v; - } - break; + return unWrapBreak(v, node.label); } else if (v.type === 'continue') { if (v.label != null && v.label !== node.label) { return v; @@ -497,10 +482,7 @@ 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; + return unWrapBreak(v, node.label); } else if (v.type === 'continue') { if (v.label != null && v.label !== node.label) { return v; @@ -757,7 +739,15 @@ export class Interpreter { case 'break': { this.log('block:break', { scope: scope.name }); - return BREAK(node.label); + if (node.expr != null) { + const val = await this._eval(node.expr, scope, callStack); + if (isControl(val)) { + return val; + } + return BREAK(val, node.label); + } else { + return BREAK(NULL, node.label); + } } case 'continue': { diff --git a/src/node.ts b/src/node.ts index bf080404..2f656366 100644 --- a/src/node.ts +++ b/src/node.ts @@ -92,6 +92,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/expression/control-flow.ts b/src/parser/syntaxes/expression/control-flow.ts index c9d31cfa..e531d3a7 100644 --- a/src/parser/syntaxes/expression/control-flow.ts +++ b/src/parser/syntaxes/expression/control-flow.ts @@ -39,7 +39,7 @@ export function parseControlFlowExpr(s: ITokenStream): Ast.ControlFlow { * ControlFlowExprWithoutLabel = If / Match / Eval / Each / For / Loop * ``` */ -export function parseControlFlowExprWithoutLabel(s: ITokenStream): Ast.ControlFlow { +function parseControlFlowExprWithoutLabel(s: ITokenStream): Ast.ControlFlow { const tokenKind = s.getTokenKind(); switch (tokenKind) { case TokenKind.IfKeyword: { diff --git a/src/parser/syntaxes/expression/index.ts b/src/parser/syntaxes/expression/index.ts index 93bc5eab..d820c9ef 100644 --- a/src/parser/syntaxes/expression/index.ts +++ b/src/parser/syntaxes/expression/index.ts @@ -534,7 +534,7 @@ function parseReturn(s: ITokenStream): Ast.Return { /** * ```abnf - * Break = "break" ["#" IDENT] + * Break = "break" ["#" IDENT] [Expr] * ``` */ function parseBreak(s: ITokenStream): Ast.Break { @@ -552,7 +552,19 @@ function parseBreak(s: ITokenStream): Ast.Break { s.next(); } - return NODE('break', { label }, startPos, s.getPos()); + let expr: Ast.Expression | undefined; + switch (s.getTokenKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: + case TokenKind.Comma: + case TokenKind.CloseBrace: + break; + default: { + expr = parseExpr(s, false); + } + } + + return NODE('break', { label, expr }, startPos, s.getPos()); } /** diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 4a5a103a..7707108b 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -23,18 +23,9 @@ export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression case TokenKind.Out: { return parseOut(s); } - case TokenKind.ReturnKeyword: { - return parseReturn(s); - } case TokenKind.OpenSharpBracket: { return parseStatementWithAttr(s); } - case TokenKind.BreakKeyword: { - return parseBreak(s); - } - case TokenKind.ContinueKeyword: { - return parseContinue(s); - } } const expr = parseExpr(s, false); const assign = tryParseAssign(s, expr); @@ -164,21 +155,6 @@ function parseOut(s: ITokenStream): Ast.Call { return CALL_NODE('print', [expr], startPos, s.getPos()); } -/** - * ```abnf - * Return = "return" Expr - * ``` -*/ -function parseReturn(s: ITokenStream): Ast.Return { - const startPos = s.getPos(); - - s.expect(TokenKind.ReturnKeyword); - s.next(); - const expr = parseExpr(s, false); - - return NODE('return', { expr }, startPos, s.getPos()); -} - /** * ```abnf * StatementWithAttr = *Attr Statement @@ -235,52 +211,6 @@ function parseAttr(s: ITokenStream): Ast.Attribute { return NODE('attr', { name, value }, 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 cf56e651..8364d7d8 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -645,6 +645,39 @@ describe('break', () => { `)); }); + describe('with expr', () => { + test.concurrent('in each', async () => { + const res = await exe(` + <: each let v, [0] { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('in for', async () => { + const res = await exe(` + <: for 1 { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('in loop', async () => { + const res = await exe(` + <: loop { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('toplevel', async () => { + assert.rejects(() => exe('break 1')); + }); + }); + describe('labeled each', () => { test.concurrent('inner each', async () => { const res = await exe(` @@ -1040,6 +1073,67 @@ describe('break', () => { `); eq(res, NULL); }); + + describe('with expr', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + <: #l: if true { + each let v, [0] { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + <: #l: if true { + for 1 { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + <: #l: if true { + loop { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner if', async () => { + const res = await exe(` + <: #l: if true { + if true { + break #l 1 + 2 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner match', async () => { + const res = await exe(` + <: #l: if true { + match 0 { + default => { + break #l 1 + 2 + } + } + } + `); + eq(res, NUM(1)); + }); + }); }); describe('labeled match', () => { From e027db0afff04ed0213c180be2404fb30b4e1e97 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 21:17:13 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/jump-statements.ts | 101 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 8364d7d8..80215f34 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -462,6 +462,28 @@ describe('return', () => { }); }); + test.concurrent('in plus', async () => { + const res = await exe(` + @f() { + let b = +(return true) + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: +(return true)')); + }); + + test.concurrent('in minus', async () => { + const res = await exe(` + @f() { + let b = -(return true) + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: -(return true)')); + }); + test.concurrent('in not', async () => { const res = await exe(` @f() { @@ -506,6 +528,23 @@ describe('return', () => { assert.rejects(() => exe('return (return 1) + 2')); }); + test.concurrent('in break', async () => { + const res = await exe(` + @f() { + for 1 { + break (return 1) + 2 + } + } + <: f() + `) + eq(res, NUM(1)); + assert.rejects(() => exe(` + for 1 { + break (return 1) + 2 + } + `)); + }); + describe('in and', async () => { test.concurrent('left', async () => { const res = await exe(` @@ -655,7 +694,7 @@ describe('break', () => { eq(res, NUM(1)); }); - test.concurrent('in for', async () => { + test.concurrent('in for times', async () => { const res = await exe(` <: for 1 { break 1 @@ -664,6 +703,15 @@ describe('break', () => { eq(res, NUM(1)); }); + test.concurrent('in for range', async () => { + const res = await exe(` + <: for let i, 1 { + break 1 + } + `); + eq(res, NUM(1)); + }); + test.concurrent('in loop', async () => { const res = await exe(` <: loop { @@ -1269,7 +1317,7 @@ describe('continue', () => { eq(res, NUM(1)); }); - test.concurrent('inner for', async () => { + test.concurrent('inner for times', async () => { const res = await exe(` var x = 0 #l: each let v, [0] { @@ -1284,6 +1332,21 @@ describe('continue', () => { eq(res, NUM(1)); }); + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + test.concurrent('inner loop', async () => { const res = await exe(` var x = 0 @@ -1346,7 +1409,7 @@ describe('continue', () => { eq(res, NUM(1)); }); - test.concurrent('inner for', async () => { + test.concurrent('inner for time', async () => { const res = await exe(` var x = 0 #l: for 1 { @@ -1361,6 +1424,21 @@ describe('continue', () => { eq(res, NUM(1)); }); + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + test.concurrent('inner loop', async () => { const res = await exe(` var x = 0 @@ -1423,7 +1501,7 @@ describe('continue', () => { eq(res, NUM(1)); }); - test.concurrent('inner for', async () => { + test.concurrent('inner for time', async () => { const res = await exe(` var x = 0 #l: while x == 0 { @@ -1438,6 +1516,21 @@ describe('continue', () => { eq(res, NUM(1)); }); + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + test.concurrent('inner loop', async () => { const res = await exe(` var x = 0 From b09e56df616bdf386d3ec714892ae2777b0290f4 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 21:21:26 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/common.ts | 2 +- .../syntaxes/{expression => }/control-flow.ts | 16 ++++++++-------- .../{expression/index.ts => expression.ts} | 14 +++++++------- src/parser/syntaxes/statements.ts | 2 +- src/parser/syntaxes/toplevel.ts | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) rename src/parser/syntaxes/{expression => }/control-flow.ts (96%) rename src/parser/syntaxes/{expression/index.ts => expression.ts} (97%) diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b183a87f..05829951 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -2,7 +2,7 @@ import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { NODE } from '../utils.js'; import { parseStatement } from './statements.js'; -import { parseExpr } from './expression/index.js'; +import { parseExpr } from './expression.js'; import type { ITokenStream } from '../streams/token-stream.js'; import type * as Ast from '../../node.js'; diff --git a/src/parser/syntaxes/expression/control-flow.ts b/src/parser/syntaxes/control-flow.ts similarity index 96% rename from src/parser/syntaxes/expression/control-flow.ts rename to src/parser/syntaxes/control-flow.ts index e531d3a7..956a1a7a 100644 --- a/src/parser/syntaxes/expression/control-flow.ts +++ b/src/parser/syntaxes/control-flow.ts @@ -1,15 +1,15 @@ /** - * 制御構造 + * 制御構造式 */ -import { AiScriptSyntaxError } from '../../../error.js'; -import { TokenKind } from '../../token.js'; -import { unexpectedTokenError, NODE } from '../../utils.js'; -import { parseBlock, parseDest, parseOptionalSeparator } from '../common.js'; -import { parseExpr } from './index.js'; +import { AiScriptSyntaxError } from '../../error.js'; +import { TokenKind } from '../token.js'; +import { unexpectedTokenError, NODE } from '../utils.js'; +import { parseBlock, parseDest, parseOptionalSeparator } from './common.js'; +import { parseExpr } from './expression.js'; -import type * as Ast from '../../../node.js'; -import type { ITokenStream } from '../../streams/token-stream.js'; +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; /** * ```abnf diff --git a/src/parser/syntaxes/expression/index.ts b/src/parser/syntaxes/expression.ts similarity index 97% rename from src/parser/syntaxes/expression/index.ts rename to src/parser/syntaxes/expression.ts index d820c9ef..6cb310ca 100644 --- a/src/parser/syntaxes/expression/index.ts +++ b/src/parser/syntaxes/expression.ts @@ -1,12 +1,12 @@ -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, parseParams, parseType } from '../common.js'; +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, parseParams, parseType } from './common.js'; import { parseControlFlowExpr } from './control-flow.js'; -import type * as Ast from '../../../node.js'; -import type { ITokenStream } from '../../streams/token-stream.js'; +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; export function parseExpr(s: ITokenStream, isStatic: boolean): Ast.Expression { if (isStatic) { diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 7707108b..52a7d908 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -2,7 +2,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 { parseExpr } from './expression/index.js'; +import { parseExpr } from './expression.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index 242b4638..1ffba71c 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -2,7 +2,7 @@ import { NODE } from '../utils.js'; import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { parseDefStatement, parseStatement } from './statements.js'; -import { parseExpr } from './expression/index.js'; +import { parseExpr } from './expression.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; From 489d09e0b1e8badef18f623883724a07de603786 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 21:41:17 +0900 Subject: [PATCH 12/22] =?UTF-8?q?return=E3=81=AE=E5=BC=8F=E3=82=92?= =?UTF-8?q?=E7=9C=81=E7=95=A5=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 10 +++++++--- src/node.ts | 2 +- src/parser/syntaxes/expression.ts | 31 ++++++++++++++++++------------- src/parser/visit.ts | 4 +++- test/jump-statements.ts | 12 ++++++++++++ 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 771b594c..26ed2eb0 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -729,9 +729,13 @@ export class Interpreter { } case 'return': { - const val = await this._eval(node.expr, scope, callStack); - if (isControl(val)) { - return val; + let val: Value = NULL; + if (node.expr != null) { + const valOrControl = await this._eval(node.expr, scope, callStack); + if (isControl(valOrControl)) { + return valOrControl; + } + val = valOrControl; } this.log('block:return', { scope: scope.name, val: val }); return RETURN(val); diff --git a/src/node.ts b/src/node.ts index 2f656366..91553bc5 100644 --- a/src/node.ts +++ b/src/node.ts @@ -62,7 +62,7 @@ export type Attribute = NodeBase & { export type Return = NodeBase & { type: 'return'; // return文 - expr: Expression; // 式 + expr?: Expression; // 式 }; export type Each = NodeBase & { diff --git a/src/parser/syntaxes/expression.ts b/src/parser/syntaxes/expression.ts index 6cb310ca..9752d442 100644 --- a/src/parser/syntaxes/expression.ts +++ b/src/parser/syntaxes/expression.ts @@ -519,7 +519,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Arr { /** * ```abnf - * Return = "return" Expr + * Return = "return" [Expr] * ``` */ function parseReturn(s: ITokenStream): Ast.Return { @@ -527,7 +527,7 @@ function parseReturn(s: ITokenStream): Ast.Return { s.expect(TokenKind.ReturnKeyword); s.next(); - const expr = parseExpr(s, false); + const expr = tryParseExpr(s); return NODE('return', { expr }, startPos, s.getPos()); } @@ -552,17 +552,7 @@ function parseBreak(s: ITokenStream): Ast.Break { s.next(); } - let expr: Ast.Expression | undefined; - switch (s.getTokenKind()) { - case TokenKind.NewLine: - case TokenKind.SemiColon: - case TokenKind.Comma: - case TokenKind.CloseBrace: - break; - default: { - expr = parseExpr(s, false); - } - } + const expr = tryParseExpr(s); return NODE('break', { label, expr }, startPos, s.getPos()); } @@ -590,6 +580,21 @@ function parseContinue(s: ITokenStream): Ast.Continue { return NODE('continue', { label }, startPos, s.getPos()); } +function tryParseExpr(s: ITokenStream): Ast.Expression | undefined { + switch (s.getTokenKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: + case TokenKind.Comma: + case TokenKind.CloseParen: + case TokenKind.CloseBracket: + case TokenKind.CloseBrace: + return; + default: { + return parseExpr(s, false); + } + } +} + //#region Pratt parsing type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 36fcf3a0..4451b09b 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -15,7 +15,9 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node break; } case 'return': { - result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; + if (result.expr != null) { + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; + } break; } case 'each': { diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 80215f34..914627e3 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -17,6 +17,18 @@ describe('return', () => { assert.rejects(() => exe('return 1')); }); + test.concurrent('without expr', async () => { + const res = await exe(` + @f() { + return + 1 + } + <: f() + `); + eq(res, NULL); + assert.rejects(() => exe('return')); + }); + test.concurrent('in eval', async () => { const res = await exe(` @f() { From f86099c49eb550a240795ce13a46fc815f7b553f Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 21:42:03 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/index.ts | 4 ++-- ...lidate-jump-statements.ts => validate-jump-expressions.ts} | 2 +- test/{jump-statements.ts => jump-expressions.ts} | 0 unreleased/{jump-statements.md => jump-expressions.md} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/parser/plugins/{validate-jump-statements.ts => validate-jump-expressions.ts} (95%) rename test/{jump-statements.ts => jump-expressions.ts} (100%) rename unreleased/{jump-statements.md => jump-expressions.md} (100%) diff --git a/src/parser/index.ts b/src/parser/index.ts index 74f535fd..8c42f676 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpStatements } from './plugins/validate-jump-statements.js'; +import { validateJumpExpressions } from './plugins/validate-jump-expressions.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -22,7 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, - validateJumpStatements, + validateJumpExpressions, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-expressions.ts similarity index 95% rename from src/parser/plugins/validate-jump-statements.ts rename to src/parser/plugins/validate-jump-expressions.ts index 99f8fae0..c57b0e79 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-expressions.ts @@ -60,7 +60,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { return node; } -export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { +export function validateJumpExpressions(nodes: Ast.Node[]): Ast.Node[] { for (const node of nodes) { visitNode(node, validateNode); } diff --git a/test/jump-statements.ts b/test/jump-expressions.ts similarity index 100% rename from test/jump-statements.ts rename to test/jump-expressions.ts diff --git a/unreleased/jump-statements.md b/unreleased/jump-expressions.md similarity index 100% rename from unreleased/jump-statements.md rename to unreleased/jump-expressions.md From cb1b82face1d9d4a72ef0cb8ce343e1865c88528 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 22:42:41 +0900 Subject: [PATCH 14/22] =?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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index a1a884cb..e59eefe6 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -664,7 +664,7 @@ function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set Date: Wed, 20 Nov 2024 22:43:42 +0900 Subject: [PATCH 15/22] =?UTF-8?q?if,match,block=E3=81=AB=E5=AF=BE=E5=BF=9C?= =?UTF-8?q?=E3=81=99=E3=82=8Bcontinue=E3=82=92=E6=96=87=E6=B3=95=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 8 +++---- .../plugins/validate-jump-expressions.ts | 24 +++++++++++++------ test/jump-expressions.ts | 5 ++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 26ed2eb0..da36ee1e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -424,7 +424,7 @@ export class Interpreter { if (node.times) { const times = await this._eval(node.times, scope, callStack); if (isControl(times)) { - return times; + return unWrapBreak(times, node.label); } assertNumber(times); for (let i = 0; i < times.value; i++) { @@ -442,11 +442,11 @@ export class Interpreter { } else { const from = await this._eval(node.from!, scope, callStack); if (isControl(from)) { - return from; + return unWrapBreak(from, node.label); } const to = await this._eval(node.to!, scope, callStack); if (isControl(to)) { - return to; + return unWrapBreak(to, node.label); } assertNumber(from); assertNumber(to); @@ -474,7 +474,7 @@ export class Interpreter { case 'each': { const items = await this._eval(node.items, scope, callStack); if (isControl(items)) { - return items; + return unWrapBreak(items, node.label); } assertArray(items); for (const item of items.value) { diff --git a/src/parser/plugins/validate-jump-expressions.ts b/src/parser/plugins/validate-jump-expressions.ts index c57b0e79..a676561a 100644 --- a/src/parser/plugins/validate-jump-expressions.ts +++ b/src/parser/plugins/validate-jump-expressions.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.ControlFlow | undefined { for (let i = ancestors.length - 1; i >= 0; i--) { const ancestor = ancestors[i]!; switch (ancestor.type) { @@ -13,7 +13,7 @@ function isInValidLoopScope(ancestors: Ast.Node[], label?: string): boolean { if (label != null && label !== ancestor.label) { continue; } - return true; + return ancestor; } case 'if': case 'match': @@ -21,13 +21,13 @@ function isInValidLoopScope(ancestors: Ast.Node[], label?: string): boolean { if (label == null || label !== ancestor.label) { continue; } - return true; + return ancestor; } case 'fn': - return false; + return; } } - return false; + return; } function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { @@ -39,7 +39,7 @@ 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); } @@ -48,11 +48,21 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 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/test/jump-expressions.ts b/test/jump-expressions.ts index 914627e3..0ba6b95f 100644 --- a/test/jump-expressions.ts +++ b/test/jump-expressions.ts @@ -1311,6 +1311,11 @@ describe('continue', () => { continue #l } `)); + assert.rejects(() => exe(` + #l: eval { + continue #l + } + `)); }); describe('labeled each', () => { From 55ffb5137720df21430049cb725e06b2dbb8adfc Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 22:50:58 +0900 Subject: [PATCH 16/22] =?UTF-8?q?CHANGELOG=E3=81=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unreleased/control-flow.md | 25 +++++++++++++++++++++++++ unreleased/jump-expressions.md | 6 ------ 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 unreleased/control-flow.md delete mode 100644 unreleased/jump-expressions.md diff --git a/unreleased/control-flow.md b/unreleased/control-flow.md new file mode 100644 index 00000000..2d75b19d --- /dev/null +++ b/unreleased/control-flow.md @@ -0,0 +1,25 @@ +- 制御構文の変更 + - each, for, loop, do-while, while, return, break, continueは式になりました。 + - each式、for式、loop式、do-while式、while式の評価値はnullまたはbreakで返された値になります。 + - return式、break式、continue式は評価するとブロックから脱出するため、値はありません。 + - **Breaking Change** 内部に文や式を記述できる以下の構文に波括弧が必須になりました。 + - if式において、then節、elif節、else節 + - match式のcase節、default節の右辺において、文を指定する場合 (式を指定する場合、波括弧は不要) + - each式、for式、loop式、do-while式、while式の処理内容 + - **Breaking Change** return式、break式、continue式の挙動が変更されました。 + - return式は関数スコープ内でないと文法エラーになります。 + - ラベルが指定されていないbreak式およびcontinue式は反復処理式(for式、each式、loop式、while式、do-while式)のスコープ内でないと文法エラーになります。 + - return式は常に関数から脱出します。 + - ラベルが指定されていないbreak式は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 + - ラベルが指定されていないcontinue式は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 + - break式、continue式にラベルを付けて処理を中断するブロックを指定できるようになりました。 + - 処理を中断したいif式、match式、each式、for式、loop式、do-while式、while式にラベルをつけることができます。 + - break式、continue式に処理を中断したい式につけたラベルを指定することができます。 + - ラベルが指定されているbreak式およびcontinue式は同一ラベルが付与された式内にないとエラーになります。 + - if式、match式はbreak式で脱出された場合、その評価値はbreakで返された値になります。 + - if式、match式にcontinue式を用いることはできません。(ラベルを対応させると文法エラー) + - break式に値を指定できるようになりました。 + - 指定された値は脱出した式の評価値になります。 + - 値を省略した場合、nullが指定されたものとみなします。 + - return式の値を省略できるようになりました。 + - 値を省略した場合、nullが指定されたものとみなします。 diff --git a/unreleased/jump-expressions.md b/unreleased/jump-expressions.md deleted file mode 100644 index eb1c1233..00000000 --- a/unreleased/jump-expressions.md +++ /dev/null @@ -1,6 +0,0 @@ -- **Breaking Change** return文、break文、continue文の挙動が変更されました。 - - return文は関数スコープ内でないと文法エラーになります。 - - break文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 - - return文は常に関数から脱出します。 - - break文は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 - - continue文は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 From b77e8442a8e0103add4235b9163aea2b5e890be8 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 20 Nov 2024 23:00:33 +0900 Subject: [PATCH 17/22] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E3=81=AE=E5=BE=A9=E5=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/common.ts | 2 +- src/parser/syntaxes/control-flow.ts | 2 +- src/parser/syntaxes/{expression.ts => expressions.ts} | 0 src/parser/syntaxes/statements.ts | 2 +- src/parser/syntaxes/toplevel.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/parser/syntaxes/{expression.ts => expressions.ts} (100%) diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 05829951..a9216823 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -2,7 +2,7 @@ import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { NODE } from '../utils.js'; import { parseStatement } from './statements.js'; -import { parseExpr } from './expression.js'; +import { parseExpr } from './expressions.js'; import type { ITokenStream } from '../streams/token-stream.js'; import type * as Ast from '../../node.js'; diff --git a/src/parser/syntaxes/control-flow.ts b/src/parser/syntaxes/control-flow.ts index 956a1a7a..5744c5ac 100644 --- a/src/parser/syntaxes/control-flow.ts +++ b/src/parser/syntaxes/control-flow.ts @@ -6,7 +6,7 @@ import { AiScriptSyntaxError } from '../../error.js'; import { TokenKind } from '../token.js'; import { unexpectedTokenError, NODE } from '../utils.js'; import { parseBlock, parseDest, parseOptionalSeparator } from './common.js'; -import { parseExpr } from './expression.js'; +import { parseExpr } from './expressions.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; diff --git a/src/parser/syntaxes/expression.ts b/src/parser/syntaxes/expressions.ts similarity index 100% rename from src/parser/syntaxes/expression.ts rename to src/parser/syntaxes/expressions.ts diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 52a7d908..4a25de95 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -2,7 +2,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 { parseExpr } from './expression.js'; +import { parseExpr } from './expressions.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index 1ffba71c..91a07787 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -2,7 +2,7 @@ import { NODE } from '../utils.js'; import { TokenKind } from '../token.js'; import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { parseDefStatement, parseStatement } from './statements.js'; -import { parseExpr } from './expression.js'; +import { parseExpr } from './expressions.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; From 40cb909139ec0d18f9423d0b1871f1939d44d626 Mon Sep 17 00:00:00 2001 From: takejohn Date: Thu, 21 Nov 2024 13:57:41 +0900 Subject: [PATCH 18/22] =?UTF-8?q?node.ts=E3=81=AE=E6=95=B4=E7=90=86?= =?UTF-8?q?=E3=81=A8=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 18 ++-- src/node.ts | 213 +++++++++++++++++++++++++++----------------- 2 files changed, 141 insertions(+), 90 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index e59eefe6..a3a6ffb5 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -145,17 +145,20 @@ declare namespace Ast { Statement, Definition, Attribute, - Return, - Each, - For, - Loop, - Break, - Continue, AddAssign, SubAssign, Assign, Expression, ControlFlow, + If, + Match, + Block, + Each, + For, + Loop, + Break, + Continue, + Return, Plus, Minus, Not, @@ -173,10 +176,7 @@ declare namespace Ast { Neq, And, Or, - If, Fn, - Match, - Block, Exists, Tmpl, Str, diff --git a/src/node.ts b/src/node.ts index 91553bc5..14ff6393 100644 --- a/src/node.ts +++ b/src/node.ts @@ -38,11 +38,20 @@ export type Statement = AddAssign | SubAssign; -const statementTypes = [ - 'def', 'return', 'each', 'for', 'loop', 'break', 'continue', 'assign', 'addAssign', 'subAssign', -]; export function isStatement(x: Node): x is Statement { - return statementTypes.includes(x.type); + switch (x.type) { + case 'def': + case 'assign': + case 'addAssign': + case 'subAssign': { + x satisfies Statement; + return true; + } + default: { + x satisfies Exclude; + return false; + } + } } export type Definition = NodeBase & { @@ -60,46 +69,6 @@ export type Attribute = NodeBase & { value: Expression; // 値 }; -export type Return = NodeBase & { - type: 'return'; // return文 - expr?: Expression; // 式 -}; - -export type Each = NodeBase & { - type: 'each'; // each文 - label?: string; // ラベル - var: Expression; // イテレータ宣言 - items: Expression; // 配列 - for: Block; // 本体処理 -}; - -export type For = NodeBase & { - type: 'for'; // for文 - label?: string; // ラベル - var?: string; // イテレータ変数名 - from?: Expression; // 開始値 - to?: Expression; // 終値 - times?: Expression; // 回数 - for: Block; // 本体処理 -}; - -export type Loop = NodeBase & { - type: 'loop'; // loop文 - label?: string; // ラベル - statements: (Statement | Expression)[]; // 処理 -}; - -export type Break = NodeBase & { - type: 'break'; // break文 - label?: string; // ラベル - expr?: Expression; // 式 -}; - -export type Continue = NodeBase & { - type: 'continue'; // continue文 - label?: string; // ラベル -}; - export type AddAssign = NodeBase & { type: 'addAssign'; // 加算代入文 dest: Expression; // 代入先 @@ -156,6 +125,57 @@ export type Expression = Index | Prop; +export function isExpression(x: Node): x is Expression { + switch (x.type) { + case 'if': + case 'match': + case 'block': + case 'each': + case 'for': + case 'loop': + case 'return': + case 'break': + case 'continue': + case 'fn': + case 'exists': + case 'tmpl': + case 'str': + case 'num': + case 'bool': + case 'null': + case 'obj': + case 'arr': + case 'plus': + case 'minus': + case 'not': + case 'pow': + case 'mul': + case 'div': + case 'rem': + case 'add': + case 'sub': + case 'lt': + case 'lteq': + case 'gt': + case 'gteq': + case 'eq': + case 'neq': + case 'and': + case 'or': + case 'identifier': + case 'call': + case 'index': + case 'prop': { + x satisfies Expression; + return true; + } + default: { + x satisfies Exclude; + return false; + } + } +} + export type ControlFlow = If | Match | @@ -164,14 +184,74 @@ export type ControlFlow = For | Loop; -const expressionTypes = [ - 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', - 'not', 'pow', 'mul', 'div', 'rem', 'add', 'sub', 'lt', 'lteq', 'gt', 'gteq', 'eq', 'neq', 'and', 'or', - 'identifier', 'call', 'index', 'prop', -]; -export function isExpression(x: Node): x is Expression { - return expressionTypes.includes(x.type); -} +export type If = NodeBase & { + type: 'if'; // if式 + label?: string; // ラベル + cond: Expression; // 条件式 + then: Block; // then節 + elseif: { + cond: Expression; // elifの条件式 + then: Block;// elif節 + }[]; + else?: Block; // else節 +}; + +export type Match = NodeBase & { + type: 'match'; // パターンマッチ + label?: string; // ラベル + about: Expression; // 対象 + qs: { + q: Expression; // 条件 + a: Expression; // 結果 + }[]; + default?: Expression; // デフォルト値 +}; + +export type Block = NodeBase & { + type: 'block'; // ブロックまたはeval式 + label?: string; // ラベル + statements: (Statement | Expression)[]; // 処理 +}; + +export type Each = NodeBase & { + type: 'each'; // each文 + label?: string; // ラベル + var: Expression; // イテレータ宣言 + items: Expression; // 配列 + for: Block; // 本体処理 +}; + +export type For = NodeBase & { + type: 'for'; // for文 + label?: string; // ラベル + var?: string; // イテレータ変数名 + from?: Expression; // 開始値 + to?: Expression; // 終値 + times?: Expression; // 回数 + for: Block; // 本体処理 +}; + +export type Loop = NodeBase & { + type: 'loop'; // loop文 + label?: string; // ラベル + statements: (Statement | Expression)[]; // 処理 +}; + +export type Break = NodeBase & { + type: 'break'; // break文 + label?: string; // ラベル + expr?: Expression; // 式 +}; + +export type Continue = NodeBase & { + type: 'continue'; // continue文 + label?: string; // ラベル +}; + +export type Return = NodeBase & { + type: 'return'; // return文 + expr?: Expression; // 式 +}; export type Plus = NodeBase & { type: 'plus'; // 正号 @@ -272,18 +352,6 @@ export type Or = NodeBase & { right: Expression; } -export type If = NodeBase & { - type: 'if'; // if式 - label?: string; // ラベル - cond: Expression; // 条件式 - then: Block; // then節 - elseif: { - cond: Expression; // elifの条件式 - then: Block;// elif節 - }[]; - else?: Block; // else節 -}; - export type Fn = NodeBase & { type: 'fn'; // 関数 params: { @@ -296,23 +364,6 @@ export type Fn = NodeBase & { children: (Statement | Expression)[]; // 本体処理 }; -export type Match = NodeBase & { - type: 'match'; // パターンマッチ - label?: string; // ラベル - about: Expression; // 対象 - qs: { - q: Expression; // 条件 - a: Expression; // 結果 - }[]; - default?: Expression; // デフォルト値 -}; - -export type Block = NodeBase & { - type: 'block'; // ブロックまたはeval式 - label?: string; // ラベル - statements: (Statement | Expression)[]; // 処理 -}; - export type Exists = NodeBase & { type: 'exists'; // 変数の存在判定 identifier: Identifier; // 変数名 From 793fc8153c70804e501cdddf0d504622c6f7f56f Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 22 Nov 2024 19:05:28 +0900 Subject: [PATCH 19/22] =?UTF-8?q?Revert=20"=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=90=8D=E5=A4=89=E6=9B=B4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f86099c49eb550a240795ce13a46fc815f7b553f. --- src/parser/index.ts | 4 ++-- ...lidate-jump-expressions.ts => validate-jump-statements.ts} | 2 +- test/{jump-expressions.ts => jump-statements.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/parser/plugins/{validate-jump-expressions.ts => validate-jump-statements.ts} (96%) rename test/{jump-expressions.ts => jump-statements.ts} (100%) diff --git a/src/parser/index.ts b/src/parser/index.ts index 8c42f676..74f535fd 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpExpressions } from './plugins/validate-jump-expressions.js'; +import { validateJumpStatements } from './plugins/validate-jump-statements.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -22,7 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, - validateJumpExpressions, + validateJumpStatements, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-expressions.ts b/src/parser/plugins/validate-jump-statements.ts similarity index 96% rename from src/parser/plugins/validate-jump-expressions.ts rename to src/parser/plugins/validate-jump-statements.ts index a676561a..b1e826d1 100644 --- a/src/parser/plugins/validate-jump-expressions.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -70,7 +70,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { return node; } -export function validateJumpExpressions(nodes: Ast.Node[]): Ast.Node[] { +export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { for (const node of nodes) { visitNode(node, validateNode); } diff --git a/test/jump-expressions.ts b/test/jump-statements.ts similarity index 100% rename from test/jump-expressions.ts rename to test/jump-statements.ts From 20b5d4134873f2c0645d0afd4179332b1e4313ba Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 22 Nov 2024 19:09:51 +0900 Subject: [PATCH 20/22] git mv --- src/parser/index.ts | 2 +- ...validate-jump-statements.ts => validate-jump-expressions.ts} | 0 test/{jump-statements.ts => jump-expressions.ts} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/parser/plugins/{validate-jump-statements.ts => validate-jump-expressions.ts} (100%) rename test/{jump-statements.ts => jump-expressions.ts} (100%) diff --git a/src/parser/index.ts b/src/parser/index.ts index 74f535fd..c154e1c0 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpStatements } from './plugins/validate-jump-statements.js'; +import { validateJumpStatements } from './plugins/validate-jump-expressions.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-expressions.ts similarity index 100% rename from src/parser/plugins/validate-jump-statements.ts rename to src/parser/plugins/validate-jump-expressions.ts diff --git a/test/jump-statements.ts b/test/jump-expressions.ts similarity index 100% rename from test/jump-statements.ts rename to test/jump-expressions.ts From 4b1b98ade5d67a5b27a66bb04e82753bf4711bcc Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 22 Nov 2024 19:10:38 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E9=96=A2=E6=95=B0=E5=90=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/index.ts | 4 ++-- src/parser/plugins/validate-jump-expressions.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser/index.ts b/src/parser/index.ts index c154e1c0..8c42f676 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpStatements } from './plugins/validate-jump-expressions.js'; +import { validateJumpExpressions } from './plugins/validate-jump-expressions.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -22,7 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, - validateJumpStatements, + validateJumpExpressions, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-expressions.ts b/src/parser/plugins/validate-jump-expressions.ts index b1e826d1..a676561a 100644 --- a/src/parser/plugins/validate-jump-expressions.ts +++ b/src/parser/plugins/validate-jump-expressions.ts @@ -70,7 +70,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { return node; } -export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { +export function validateJumpExpressions(nodes: Ast.Node[]): Ast.Node[] { for (const node of nodes) { visitNode(node, validateNode); } From 41d5f8a48b45417c516d5da8fb57d3a5a43ca3d8 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 23 Nov 2024 00:43:36 +0900 Subject: [PATCH 22/22] Revert "git mv" This reverts commit 20b5d4134873f2c0645d0afd4179332b1e4313ba. --- src/parser/index.ts | 2 +- ...validate-jump-expressions.ts => validate-jump-statements.ts} | 0 test/{jump-expressions.ts => jump-statements.ts} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/parser/plugins/{validate-jump-expressions.ts => validate-jump-statements.ts} (100%) rename test/{jump-expressions.ts => jump-statements.ts} (100%) diff --git a/src/parser/index.ts b/src/parser/index.ts index 8c42f676..6c11dee6 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpExpressions } from './plugins/validate-jump-expressions.js'; +import { validateJumpExpressions } from './plugins/validate-jump-statements.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; diff --git a/src/parser/plugins/validate-jump-expressions.ts b/src/parser/plugins/validate-jump-statements.ts similarity index 100% rename from src/parser/plugins/validate-jump-expressions.ts rename to src/parser/plugins/validate-jump-statements.ts diff --git a/test/jump-expressions.ts b/test/jump-statements.ts similarity index 100% rename from test/jump-expressions.ts rename to test/jump-statements.ts