Skip to content

Commit a4a0b33

Browse files
authored
enhance: オブジェクトリテラルのキーに予約語を直接記述できるように (#948)
* オブジェクトリテラルのキーに予約語を直接記述できるように * lint修正 * default節に波括弧を追加 * exhaustiveness checkのErrorをTypeErrorに変更
1 parent 661e7d7 commit a4a0b33

4 files changed

Lines changed: 129 additions & 7 deletions

File tree

src/parser/syntaxes/expressions.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js';
22
import { NODE, unexpectedTokenError } from '../utils.js';
33
import { TokenStream } from '../streams/token-stream.js';
4-
import { TokenKind } from '../token.js';
4+
import { isKeywordTokenKind, keywordTokenKindToString, TokenKind } from '../token.js';
55
import { parseBlock, parseLabel, parseOptionalSeparator, parseParams } from './common.js';
66
import { parseBlockOrStatement } from './statements.js';
77
import { parseType, parseTypeParams } from './types.js';
@@ -577,7 +577,7 @@ function parseReference(s: ITokenStream): Ast.Identifier {
577577

578578
/**
579579
* ```abnf
580-
* Object = "{" [IDENT ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}"
580+
* Object = "{" [ObjectKey ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}"
581581
* ```
582582
*/
583583
function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj {
@@ -592,11 +592,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj {
592592

593593
const map = new Map<string, Ast.Expression>();
594594
while (!s.is(TokenKind.CloseBrace)) {
595-
const keyTokenKind = s.getTokenKind();
596-
if (keyTokenKind !== TokenKind.Identifier && keyTokenKind !== TokenKind.StringLiteral) {
597-
throw unexpectedTokenError(keyTokenKind, s.getPos());
598-
}
599-
const k = s.getTokenValue();
595+
const k = parseObjectKey(s);
600596
s.next();
601597

602598
s.expect(TokenKind.Colon);
@@ -634,6 +630,29 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj {
634630
return NODE('obj', { value: map }, startPos, s.getPos());
635631
}
636632

633+
/**
634+
* ```abnf
635+
* ObjectKey = IDENT / StringLiteral / Keyword
636+
* ```
637+
*/
638+
function parseObjectKey(s: ITokenStream): string {
639+
const tokenKind = s.getTokenKind();
640+
641+
if (tokenKind === TokenKind.Identifier) {
642+
return s.getTokenValue();
643+
}
644+
645+
if (tokenKind === TokenKind.StringLiteral) {
646+
return s.getTokenValue();
647+
}
648+
649+
if (isKeywordTokenKind(tokenKind)) {
650+
return keywordTokenKindToString(tokenKind);
651+
}
652+
653+
throw unexpectedTokenError(tokenKind, s.getPos());
654+
}
655+
637656
/**
638657
* ```abnf
639658
* Array = "[" [Expr *(SEP Expr) [SEP]] "]"

src/parser/token.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,64 @@ export class Token {
134134
export function TOKEN(kind: TokenKind, pos: TokenPosition, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token {
135135
return new Token(kind, pos, opts?.hasLeftSpacing, opts?.value, opts?.children);
136136
}
137+
138+
const KEYWORDS = [
139+
TokenKind.NullKeyword,
140+
TokenKind.TrueKeyword,
141+
TokenKind.FalseKeyword,
142+
TokenKind.EachKeyword,
143+
TokenKind.ForKeyword,
144+
TokenKind.LoopKeyword,
145+
TokenKind.DoKeyword,
146+
TokenKind.WhileKeyword,
147+
TokenKind.BreakKeyword,
148+
TokenKind.ContinueKeyword,
149+
TokenKind.MatchKeyword,
150+
TokenKind.CaseKeyword,
151+
TokenKind.DefaultKeyword,
152+
TokenKind.IfKeyword,
153+
TokenKind.ElifKeyword,
154+
TokenKind.ElseKeyword,
155+
TokenKind.ReturnKeyword,
156+
TokenKind.EvalKeyword,
157+
TokenKind.VarKeyword,
158+
TokenKind.LetKeyword,
159+
TokenKind.ExistsKeyword,
160+
] as const;
161+
162+
export type KeywordTokenKind = (typeof KEYWORDS)[number];
163+
164+
export function isKeywordTokenKind(token: TokenKind): token is KeywordTokenKind {
165+
return (KEYWORDS as readonly TokenKind[]).includes(token);
166+
}
167+
168+
export function keywordTokenKindToString(token: KeywordTokenKind): string {
169+
switch (token) {
170+
case TokenKind.NullKeyword: return 'null';
171+
case TokenKind.TrueKeyword: return 'true';
172+
case TokenKind.FalseKeyword: return 'false';
173+
case TokenKind.EachKeyword: return 'each';
174+
case TokenKind.ForKeyword: return 'for';
175+
case TokenKind.LoopKeyword: return 'loop';
176+
case TokenKind.DoKeyword: return 'do';
177+
case TokenKind.WhileKeyword: return 'while';
178+
case TokenKind.BreakKeyword: return 'break';
179+
case TokenKind.ContinueKeyword: return 'continue';
180+
case TokenKind.MatchKeyword: return 'match';
181+
case TokenKind.CaseKeyword: return 'case';
182+
case TokenKind.DefaultKeyword: return 'default';
183+
case TokenKind.IfKeyword: return 'if';
184+
case TokenKind.ElifKeyword: return 'elif';
185+
case TokenKind.ElseKeyword: return 'else';
186+
case TokenKind.ReturnKeyword: return 'return';
187+
case TokenKind.EvalKeyword: return 'eval';
188+
case TokenKind.VarKeyword: return 'var';
189+
case TokenKind.LetKeyword: return 'let';
190+
case TokenKind.ExistsKeyword: return 'exists';
191+
default: {
192+
// exhaustiveness check
193+
const _token: never = token;
194+
throw new TypeError(`Unknown keyword token kind ${_token}`);
195+
}
196+
}
197+
}

test/literals.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,47 @@ describe('literal', () => {
139139
eq(res, OBJ(new Map([['藍', NUM(42)]])));
140140
});
141141

142+
describe('obj (reserved word as key)', async () => {
143+
test.each([
144+
['null'],
145+
['true'],
146+
['false'],
147+
['each'],
148+
['for'],
149+
['loop'],
150+
['do'],
151+
['while'],
152+
['break'],
153+
['continue'],
154+
['match'],
155+
['case'],
156+
['default'],
157+
['if'],
158+
['elif'],
159+
['else'],
160+
['return'],
161+
['eval'],
162+
['var'],
163+
['let'],
164+
['exists'],
165+
])('key "%s"', async (key) => {
166+
const res = await exe(`
167+
<: {
168+
${key}: 42,
169+
}
170+
`);
171+
eq(res, OBJ(new Map([[key, NUM(42)]])));
172+
});
173+
});
174+
175+
test.concurrent('obj (invalid key)', async () => {
176+
assert.rejects(() => exe(`
177+
<: {
178+
42: 42,
179+
}
180+
`));
181+
});
182+
142183
test.concurrent('obj and arr (separated by line break)', async () => {
143184
const res = await exe(`
144185
<: {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- オブジェクトリテラルのプロパティ名に予約語を直接記述できるようになりました。

0 commit comments

Comments
 (0)