From 9f43986c70d57d1d78e4636c332a27b61b559e0f Mon Sep 17 00:00:00 2001 From: Take-John <105504345+takejohn@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:32:45 +0900 Subject: [PATCH] =?UTF-8?q?match=E5=BC=8F=E3=81=AEdefault=E7=AF=80?= =?UTF-8?q?=E3=81=AE=E5=89=8D=E3=81=AB=E5=8C=BA=E5=88=87=E3=82=8A=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=82=92=E5=BC=B7=E5=88=B6=20(#825)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/common.ts | 24 ++++++ src/parser/syntaxes/expressions.ts | 102 +++++++++++-------------- test/syntax.ts | 18 +++++ unreleased/match-sep-before-default.md | 1 + 4 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 unreleased/match-sep-before-default.md diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 9b1bc54e..c94226b1 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -138,6 +138,30 @@ export function parseType(s: ITokenStream): Ast.TypeSource { } } +/** + * ```abnf + * OptionalSeparator = [SEP] + * ``` +*/ +export function parseOptionalSeparator(s: ITokenStream): boolean { + switch (s.getTokenKind()) { + case TokenKind.NewLine: { + s.next(); + return true; + } + case TokenKind.Comma: { + s.next(); + if (s.is(TokenKind.NewLine)) { + s.next(); + } + return true; + } + default: { + return false; + } + } +} + /** * ```abnf * FnType = "@" "(" ParamTypes ")" "=>" Type diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 342644ab..2736e0f5 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -2,7 +2,7 @@ import { AiScriptSyntaxError } from '../../error.js'; import { NODE } from '../utils.js'; import { TokenStream } from '../streams/token-stream.js'; import { TokenKind } from '../token.js'; -import { parseBlock, parseParams, parseType } from './common.js'; +import { parseBlock, parseOptionalSeparator, parseParams, parseType } from './common.js'; import { parseBlockOrStatement } from './statements.js'; import type * as Ast from '../../node.js'; @@ -404,9 +404,7 @@ function parseFnExpr(s: ITokenStream): Ast.Fn { /** * ```abnf - * Match = "match" Expr "{" [MatchCases] [defaultCase] "}" - * MatchCases = "case" Expr "=>" BlockOrStatement *(SEP "case" Expr "=>" BlockOrStatement) [SEP] - * DefaultCase = "default" "=>" BlockOrStatement [SEP] + * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}" * ``` */ function parseMatch(s: ITokenStream): Ast.Match { @@ -424,65 +422,27 @@ function parseMatch(s: ITokenStream): Ast.Match { } const qs: Ast.Match['qs'] = []; - while (!s.is(TokenKind.DefaultKeyword) && !s.is(TokenKind.CloseBrace)) { - s.expect(TokenKind.CaseKeyword); - s.next(); - const q = parseExpr(s, false); - s.expect(TokenKind.Arrow); - s.next(); - const a = parseBlockOrStatement(s); - qs.push({ q, a }); - - // separator - switch (s.getTokenKind()) { - case TokenKind.NewLine: { - s.next(); - break; - } - case TokenKind.Comma: { - s.next(); - if (s.is(TokenKind.NewLine)) { - s.next(); - } - break; - } - case TokenKind.DefaultKeyword: - case TokenKind.CloseBrace: { - break; - } - default: { + 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); } - } - - let x: Ast.Match['default']; - if (s.is(TokenKind.DefaultKeyword)) { - s.next(); - s.expect(TokenKind.Arrow); - s.next(); - x = parseBlockOrStatement(s); - - // separator - switch (s.getTokenKind()) { - case TokenKind.NewLine: { - s.next(); - break; - } - case TokenKind.Comma: { - s.next(); - if (s.is(TokenKind.NewLine)) { - s.next(); - } - break; - } - case TokenKind.CloseBrace: { - break; - } - default: { + 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); @@ -491,6 +451,34 @@ function parseMatch(s: ITokenStream): Ast.Match { 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 diff --git a/test/syntax.ts b/test/syntax.ts index 84d25539..6cee2171 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -176,6 +176,24 @@ describe('separator', () => { `); eq(res, STR('c')); }); + + test.concurrent('no separator', async () => { + await assert.rejects(async () => { + await exe(` + let x = 1 + <:match x{case 1=>"a" case 2=>"b"} + `); + }); + }); + + test.concurrent('no separator (default)', async () => { + await assert.rejects(async () => { + await exe(` + let x = 1 + <:match x{case 1=>"a" default=>"b"} + `); + }); + }); }); describe('call', () => { diff --git a/unreleased/match-sep-before-default.md b/unreleased/match-sep-before-default.md new file mode 100644 index 00000000..3b87a325 --- /dev/null +++ b/unreleased/match-sep-before-default.md @@ -0,0 +1 @@ +- **Breaking Change** match式において、case節とdefault節の間に区切り文字が必須になりました。case節の後にdefault節を区切り文字なしで続けると文法エラーになります。