diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index e249548b6..1b638781f 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -1,7 +1,7 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; import { NODE, unexpectedTokenError } from '../utils.js'; import { TokenStream } from '../streams/token-stream.js'; -import { TokenKind } from '../token.js'; +import { isKeywordTokenKind, keywordTokenKindToString, TokenKind } from '../token.js'; import { parseBlock, parseLabel, parseOptionalSeparator, parseParams } from './common.js'; import { parseBlockOrStatement } from './statements.js'; import { parseType, parseTypeParams } from './types.js'; @@ -577,7 +577,7 @@ function parseReference(s: ITokenStream): Ast.Identifier { /** * ```abnf - * Object = "{" [IDENT ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}" + * Object = "{" [ObjectKey ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}" * ``` */ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj { @@ -592,11 +592,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj { const map = new Map(); while (!s.is(TokenKind.CloseBrace)) { - const keyTokenKind = s.getTokenKind(); - if (keyTokenKind !== TokenKind.Identifier && keyTokenKind !== TokenKind.StringLiteral) { - throw unexpectedTokenError(keyTokenKind, s.getPos()); - } - const k = s.getTokenValue(); + const k = parseObjectKey(s); s.next(); s.expect(TokenKind.Colon); @@ -634,6 +630,29 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj { return NODE('obj', { value: map }, startPos, s.getPos()); } +/** + * ```abnf + * ObjectKey = IDENT / StringLiteral / Keyword + * ``` + */ +function parseObjectKey(s: ITokenStream): string { + const tokenKind = s.getTokenKind(); + + if (tokenKind === TokenKind.Identifier) { + return s.getTokenValue(); + } + + if (tokenKind === TokenKind.StringLiteral) { + return s.getTokenValue(); + } + + if (isKeywordTokenKind(tokenKind)) { + return keywordTokenKindToString(tokenKind); + } + + throw unexpectedTokenError(tokenKind, s.getPos()); +} + /** * ```abnf * Array = "[" [Expr *(SEP Expr) [SEP]] "]" diff --git a/src/parser/token.ts b/src/parser/token.ts index 35b2d4c75..1c2f2014b 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -134,3 +134,64 @@ export class Token { export function TOKEN(kind: TokenKind, pos: TokenPosition, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token { return new Token(kind, pos, opts?.hasLeftSpacing, opts?.value, opts?.children); } + +const KEYWORDS = [ + TokenKind.NullKeyword, + TokenKind.TrueKeyword, + TokenKind.FalseKeyword, + TokenKind.EachKeyword, + TokenKind.ForKeyword, + TokenKind.LoopKeyword, + TokenKind.DoKeyword, + TokenKind.WhileKeyword, + TokenKind.BreakKeyword, + TokenKind.ContinueKeyword, + TokenKind.MatchKeyword, + TokenKind.CaseKeyword, + TokenKind.DefaultKeyword, + TokenKind.IfKeyword, + TokenKind.ElifKeyword, + TokenKind.ElseKeyword, + TokenKind.ReturnKeyword, + TokenKind.EvalKeyword, + TokenKind.VarKeyword, + TokenKind.LetKeyword, + TokenKind.ExistsKeyword, +] as const; + +export type KeywordTokenKind = (typeof KEYWORDS)[number]; + +export function isKeywordTokenKind(token: TokenKind): token is KeywordTokenKind { + return (KEYWORDS as readonly TokenKind[]).includes(token); +} + +export function keywordTokenKindToString(token: KeywordTokenKind): string { + switch (token) { + case TokenKind.NullKeyword: return 'null'; + case TokenKind.TrueKeyword: return 'true'; + case TokenKind.FalseKeyword: return 'false'; + case TokenKind.EachKeyword: return 'each'; + case TokenKind.ForKeyword: return 'for'; + case TokenKind.LoopKeyword: return 'loop'; + case TokenKind.DoKeyword: return 'do'; + case TokenKind.WhileKeyword: return 'while'; + case TokenKind.BreakKeyword: return 'break'; + case TokenKind.ContinueKeyword: return 'continue'; + case TokenKind.MatchKeyword: return 'match'; + case TokenKind.CaseKeyword: return 'case'; + case TokenKind.DefaultKeyword: return 'default'; + case TokenKind.IfKeyword: return 'if'; + case TokenKind.ElifKeyword: return 'elif'; + case TokenKind.ElseKeyword: return 'else'; + case TokenKind.ReturnKeyword: return 'return'; + case TokenKind.EvalKeyword: return 'eval'; + case TokenKind.VarKeyword: return 'var'; + case TokenKind.LetKeyword: return 'let'; + case TokenKind.ExistsKeyword: return 'exists'; + default: { + // exhaustiveness check + const _token: never = token; + throw new TypeError(`Unknown keyword token kind ${_token}`); + } + } +} diff --git a/test/literals.ts b/test/literals.ts index 462cb5b7e..2167c485f 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -139,6 +139,47 @@ describe('literal', () => { eq(res, OBJ(new Map([['藍', NUM(42)]]))); }); + describe('obj (reserved word as key)', async () => { + test.each([ + ['null'], + ['true'], + ['false'], + ['each'], + ['for'], + ['loop'], + ['do'], + ['while'], + ['break'], + ['continue'], + ['match'], + ['case'], + ['default'], + ['if'], + ['elif'], + ['else'], + ['return'], + ['eval'], + ['var'], + ['let'], + ['exists'], + ])('key "%s"', async (key) => { + const res = await exe(` + <: { + ${key}: 42, + } + `); + eq(res, OBJ(new Map([[key, NUM(42)]]))); + }); + }); + + test.concurrent('obj (invalid key)', async () => { + assert.rejects(() => exe(` + <: { + 42: 42, + } + `)); + }); + test.concurrent('obj and arr (separated by line break)', async () => { const res = await exe(` <: { diff --git a/unreleased/allow-reserved-word-keys.md b/unreleased/allow-reserved-word-keys.md new file mode 100644 index 000000000..916199689 --- /dev/null +++ b/unreleased/allow-reserved-word-keys.md @@ -0,0 +1 @@ +- オブジェクトリテラルのプロパティ名に予約語を直接記述できるようになりました。