From 45412076b8ca3f3499254f12652af7a90f9030e2 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 6 Aug 2025 14:38:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E3=82=AA=E3=83=96=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=83=AA=E3=83=86=E3=83=A9=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E3=81=AB=E4=BA=88=E7=B4=84=E8=AA=9E=E3=82=92?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E8=A8=98=E8=BF=B0=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/syntaxes/expressions.ts | 33 +++++++++++--- src/parser/token.ts | 60 ++++++++++++++++++++++++++ test/literals.ts | 41 ++++++++++++++++++ unreleased/allow-reserved-word-keys.md | 1 + 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 unreleased/allow-reserved-word-keys.md 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..6425f0d1f 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -134,3 +134,63 @@ 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 Error(`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 @@ +- オブジェクトリテラルのプロパティ名に予約語を直接記述できるようになりました。 From 769d75ad61bf5afff1546196af8f05c1ce9a54f6 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 6 Aug 2025 14:51:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?lint=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/token.ts b/src/parser/token.ts index 6425f0d1f..e5633fe2c 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -156,7 +156,7 @@ const KEYWORDS = [ TokenKind.EvalKeyword, TokenKind.VarKeyword, TokenKind.LetKeyword, - TokenKind.ExistsKeyword + TokenKind.ExistsKeyword, ] as const; export type KeywordTokenKind = (typeof KEYWORDS)[number]; From cb73d5c508fd040a5dbdb6e65491b41226007422 Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 6 Aug 2025 15:11:53 +0900 Subject: [PATCH 3/4] =?UTF-8?q?default=E7=AF=80=E3=81=AB=E6=B3=A2=E6=8B=AC?= =?UTF-8?q?=E5=BC=A7=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/token.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/token.ts b/src/parser/token.ts index e5633fe2c..0fac052ad 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -188,9 +188,10 @@ export function keywordTokenKindToString(token: KeywordTokenKind): string { case TokenKind.VarKeyword: return 'var'; case TokenKind.LetKeyword: return 'let'; case TokenKind.ExistsKeyword: return 'exists'; - default: + default: { // exhaustiveness check const _token: never = token; throw new Error(`Unknown keyword token kind ${_token}`); + } } } From 358f98f43c5e2300bbbbf9b87f6f2cc3bdceaedc Mon Sep 17 00:00:00 2001 From: takejohn Date: Thu, 7 Aug 2025 10:07:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?exhaustiveness=20check=E3=81=AEError?= =?UTF-8?q?=E3=82=92TypeError=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/token.ts b/src/parser/token.ts index 0fac052ad..1c2f2014b 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -191,7 +191,7 @@ export function keywordTokenKindToString(token: KeywordTokenKind): string { default: { // exhaustiveness check const _token: never = token; - throw new Error(`Unknown keyword token kind ${_token}`); + throw new TypeError(`Unknown keyword token kind ${_token}`); } } }