Skip to content

Commit

Permalink
match式のdefault節の前に区切り文字を強制 (aiscript-dev#825)
Browse files Browse the repository at this point in the history
  • Loading branch information
takejohn authored Oct 27, 2024
1 parent d4496b3 commit 9f43986
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 57 deletions.
24 changes: 24 additions & 0 deletions src/parser/syntaxes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 45 additions & 57 deletions src/parser/syntaxes/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions test/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions unreleased/match-sep-before-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Breaking Change** match式において、case節とdefault節の間に区切り文字が必須になりました。case節の後にdefault節を区切り文字なしで続けると文法エラーになります。

0 comments on commit 9f43986

Please sign in to comment.