@prisma-next/psl-parser 0.13.0-dev.20 → 0.13.0-dev.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/syntax.d.mts +63 -6
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +699 -15
- package/dist/syntax.mjs.map +1 -1
- package/dist/tokenizer-DdaOstnz.mjs +233 -0
- package/dist/tokenizer-DdaOstnz.mjs.map +1 -0
- package/dist/tokenizer.mjs +1 -190
- package/package.json +5 -5
- package/src/exports/syntax.ts +7 -1
- package/src/parse.ts +767 -0
- package/src/source-file.ts +89 -0
- package/src/syntax/ast/declarations.ts +7 -5
- package/src/syntax/ast/expressions.ts +144 -17
- package/src/syntax/syntax-kind.ts +7 -2
- package/src/tokenizer.ts +48 -0
- package/dist/tokenizer.mjs.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const LINE_FEED = 10;
|
|
2
|
+
|
|
3
|
+
export interface Position {
|
|
4
|
+
readonly line: number;
|
|
5
|
+
readonly character: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Range {
|
|
9
|
+
readonly start: Position;
|
|
10
|
+
readonly end: Position;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SourceFile {
|
|
14
|
+
readonly #text: string;
|
|
15
|
+
readonly #lineStarts: readonly number[];
|
|
16
|
+
|
|
17
|
+
constructor(text: string) {
|
|
18
|
+
this.#text = text;
|
|
19
|
+
const lineStarts: number[] = [0];
|
|
20
|
+
for (let offset = 0; offset < text.length; offset++) {
|
|
21
|
+
if (text.charCodeAt(offset) === LINE_FEED) {
|
|
22
|
+
lineStarts.push(offset + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
this.#lineStarts = lineStarts;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get text(): string {
|
|
29
|
+
return this.#text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get length(): number {
|
|
33
|
+
return this.#text.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get lineCount(): number {
|
|
37
|
+
return this.#lineStarts.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lineStartOffsets(): readonly number[] {
|
|
41
|
+
return this.#lineStarts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
positionAt(offset: number): Position {
|
|
45
|
+
const clamped = clamp(offset, 0, this.#text.length);
|
|
46
|
+
const line = this.#lineIndexAt(clamped);
|
|
47
|
+
return { line, character: clamped - this.#lineStartAt(line) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
offsetAt(position: Position): number {
|
|
51
|
+
const line = clamp(position.line, 0, this.#lineStarts.length - 1);
|
|
52
|
+
const lineStart = this.#lineStartAt(line);
|
|
53
|
+
const lineEnd = this.#lineEndAt(line);
|
|
54
|
+
return clamp(lineStart + position.character, lineStart, lineEnd);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#lineStartAt(line: number): number {
|
|
58
|
+
return this.#lineStarts[line] ?? 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#lineEndAt(line: number): number {
|
|
62
|
+
return line + 1 < this.#lineStarts.length ? this.#lineStartAt(line + 1) - 1 : this.#text.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#lineIndexAt(offset: number): number {
|
|
66
|
+
const lineStarts = this.#lineStarts;
|
|
67
|
+
let low = 0;
|
|
68
|
+
let high = lineStarts.length - 1;
|
|
69
|
+
while (low < high) {
|
|
70
|
+
const mid = (low + high + 1) >>> 1;
|
|
71
|
+
if ((lineStarts[mid] ?? 0) <= offset) {
|
|
72
|
+
low = mid;
|
|
73
|
+
} else {
|
|
74
|
+
high = mid - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return low;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clamp(value: number, min: number, max: number): number {
|
|
82
|
+
if (value < min) {
|
|
83
|
+
return min;
|
|
84
|
+
}
|
|
85
|
+
if (value > max) {
|
|
86
|
+
return max;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
@@ -18,14 +18,14 @@ export type NamespaceMemberAst =
|
|
|
18
18
|
| ModelDeclarationAst
|
|
19
19
|
| EnumDeclarationAst
|
|
20
20
|
| CompositeTypeDeclarationAst
|
|
21
|
-
|
|
|
21
|
+
| GenericBlockDeclarationAst;
|
|
22
22
|
|
|
23
23
|
function castNamespaceMember(node: SyntaxNode): NamespaceMemberAst | undefined {
|
|
24
24
|
return (
|
|
25
25
|
ModelDeclarationAst.cast(node) ??
|
|
26
26
|
EnumDeclarationAst.cast(node) ??
|
|
27
27
|
CompositeTypeDeclarationAst.cast(node) ??
|
|
28
|
-
|
|
28
|
+
GenericBlockDeclarationAst.cast(node)
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -219,7 +219,7 @@ export class TypesBlockAst implements AstNode {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
export class
|
|
222
|
+
export class GenericBlockDeclarationAst implements AstNode {
|
|
223
223
|
readonly syntax: SyntaxNode;
|
|
224
224
|
|
|
225
225
|
constructor(syntax: SyntaxNode) {
|
|
@@ -246,8 +246,10 @@ export class BlockDeclarationAst implements AstNode {
|
|
|
246
246
|
yield* filterChildren(this.syntax, KeyValuePairAst.cast);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
static cast(node: SyntaxNode):
|
|
250
|
-
return node.kind === '
|
|
249
|
+
static cast(node: SyntaxNode): GenericBlockDeclarationAst | undefined {
|
|
250
|
+
return node.kind === 'GenericBlockDeclaration'
|
|
251
|
+
? new GenericBlockDeclarationAst(node)
|
|
252
|
+
: undefined;
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -56,6 +56,78 @@ export class ArrayLiteralAst implements AstNode {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
60
|
+
|
|
61
|
+
function decodeFixedHex(raw: string, start: number, width: number): string | undefined {
|
|
62
|
+
if (start + width > raw.length) return undefined;
|
|
63
|
+
const hex = raw.slice(start, start + width);
|
|
64
|
+
if (!HEX.test(hex)) return undefined;
|
|
65
|
+
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodeStringLiteral(raw: string): string {
|
|
69
|
+
let out = '';
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < raw.length) {
|
|
72
|
+
const ch = raw.charAt(i);
|
|
73
|
+
if (ch !== '\\' || i + 1 >= raw.length) {
|
|
74
|
+
out += ch;
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const next = raw.charAt(i + 1);
|
|
79
|
+
switch (next) {
|
|
80
|
+
case 'n':
|
|
81
|
+
out += '\n';
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
case 'r':
|
|
85
|
+
out += '\r';
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
case 't':
|
|
89
|
+
out += '\t';
|
|
90
|
+
i += 2;
|
|
91
|
+
continue;
|
|
92
|
+
case '"':
|
|
93
|
+
out += '"';
|
|
94
|
+
i += 2;
|
|
95
|
+
continue;
|
|
96
|
+
case '\\':
|
|
97
|
+
out += '\\';
|
|
98
|
+
i += 2;
|
|
99
|
+
continue;
|
|
100
|
+
case 'x': {
|
|
101
|
+
const decoded = decodeFixedHex(raw, i + 2, 2);
|
|
102
|
+
if (decoded === undefined) {
|
|
103
|
+
out += '\\x';
|
|
104
|
+
i += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out += decoded;
|
|
108
|
+
i += 4;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
case 'u': {
|
|
112
|
+
const decoded = decodeFixedHex(raw, i + 2, 4);
|
|
113
|
+
if (decoded === undefined) {
|
|
114
|
+
out += '\\u';
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
out += decoded;
|
|
119
|
+
i += 6;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
out += `\\${next}`;
|
|
124
|
+
i += 2;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
59
131
|
export class StringLiteralExprAst implements AstNode {
|
|
60
132
|
readonly syntax: SyntaxNode;
|
|
61
133
|
|
|
@@ -70,23 +142,7 @@ export class StringLiteralExprAst implements AstNode {
|
|
|
70
142
|
value(): string | undefined {
|
|
71
143
|
const tok = this.token();
|
|
72
144
|
if (!tok) return undefined;
|
|
73
|
-
|
|
74
|
-
return raw.replace(/\\(.)/g, (_match, char: string) => {
|
|
75
|
-
switch (char) {
|
|
76
|
-
case 'n':
|
|
77
|
-
return '\n';
|
|
78
|
-
case 'r':
|
|
79
|
-
return '\r';
|
|
80
|
-
case 't':
|
|
81
|
-
return '\t';
|
|
82
|
-
case '"':
|
|
83
|
-
return '"';
|
|
84
|
-
case '\\':
|
|
85
|
-
return '\\';
|
|
86
|
-
default:
|
|
87
|
-
return `\\${char}`;
|
|
88
|
-
}
|
|
89
|
-
});
|
|
145
|
+
return decodeStringLiteral(tok.text.slice(1, -1));
|
|
90
146
|
}
|
|
91
147
|
|
|
92
148
|
static cast(node: SyntaxNode): StringLiteralExprAst | undefined {
|
|
@@ -140,12 +196,82 @@ export class BooleanLiteralExprAst implements AstNode {
|
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
198
|
|
|
199
|
+
export class ObjectLiteralExprAst implements AstNode {
|
|
200
|
+
readonly syntax: SyntaxNode;
|
|
201
|
+
|
|
202
|
+
constructor(syntax: SyntaxNode) {
|
|
203
|
+
this.syntax = syntax;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
lbrace(): Token | undefined {
|
|
207
|
+
return findChildToken(this.syntax, 'LBrace');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
rbrace(): Token | undefined {
|
|
211
|
+
return findChildToken(this.syntax, 'RBrace');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
*fields(): Iterable<ObjectFieldAst> {
|
|
215
|
+
yield* filterChildren(this.syntax, ObjectFieldAst.cast);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static cast(node: SyntaxNode): ObjectLiteralExprAst | undefined {
|
|
219
|
+
return node.kind === 'ObjectLiteralExpr' ? new ObjectLiteralExprAst(node) : undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class ObjectFieldAst implements AstNode {
|
|
224
|
+
readonly syntax: SyntaxNode;
|
|
225
|
+
|
|
226
|
+
constructor(syntax: SyntaxNode) {
|
|
227
|
+
this.syntax = syntax;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
key(): IdentifierAst | undefined {
|
|
231
|
+
for (const child of this.syntax.children()) {
|
|
232
|
+
if (!(child instanceof SyntaxNode)) {
|
|
233
|
+
if (child.kind === 'Colon') break;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
return IdentifierAst.cast(child);
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
colon(): Token | undefined {
|
|
242
|
+
return findChildToken(this.syntax, 'Colon');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
value(): ExpressionAst | undefined {
|
|
246
|
+
if (this.colon()) {
|
|
247
|
+
let pastColon = false;
|
|
248
|
+
for (const child of this.syntax.children()) {
|
|
249
|
+
if (!(child instanceof SyntaxNode)) {
|
|
250
|
+
if (child.kind === 'Colon') pastColon = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (pastColon) {
|
|
254
|
+
const expr = castExpression(child);
|
|
255
|
+
if (expr) return expr;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return findFirstChild(this.syntax, castExpression);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static cast(node: SyntaxNode): ObjectFieldAst | undefined {
|
|
264
|
+
return node.kind === 'ObjectField' ? new ObjectFieldAst(node) : undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
143
268
|
export type ExpressionAst =
|
|
144
269
|
| FunctionCallAst
|
|
145
270
|
| ArrayLiteralAst
|
|
146
271
|
| StringLiteralExprAst
|
|
147
272
|
| NumberLiteralExprAst
|
|
148
273
|
| BooleanLiteralExprAst
|
|
274
|
+
| ObjectLiteralExprAst
|
|
149
275
|
| IdentifierAst;
|
|
150
276
|
|
|
151
277
|
export function castExpression(node: SyntaxNode): ExpressionAst | undefined {
|
|
@@ -155,6 +281,7 @@ export function castExpression(node: SyntaxNode): ExpressionAst | undefined {
|
|
|
155
281
|
StringLiteralExprAst.cast(node) ??
|
|
156
282
|
NumberLiteralExprAst.cast(node) ??
|
|
157
283
|
BooleanLiteralExprAst.cast(node) ??
|
|
284
|
+
ObjectLiteralExprAst.cast(node) ??
|
|
158
285
|
IdentifierAst.cast(node)
|
|
159
286
|
);
|
|
160
287
|
}
|
|
@@ -5,7 +5,10 @@ export type SyntaxKind =
|
|
|
5
5
|
| 'CompositeTypeDeclaration'
|
|
6
6
|
| 'Namespace'
|
|
7
7
|
| 'TypesBlock'
|
|
8
|
-
|
|
8
|
+
// The generic/extension block node — the `kw [name] { key = value }` form
|
|
9
|
+
// produced by `parseGenericBlock`. Deliberately distinct from the reserved
|
|
10
|
+
// `model`/`enum`/`namespace`/`type`/`types` declarations above.
|
|
11
|
+
| 'GenericBlockDeclaration'
|
|
9
12
|
| 'FieldDeclaration'
|
|
10
13
|
| 'EnumValueDeclaration'
|
|
11
14
|
| 'NamedTypeDeclaration'
|
|
@@ -20,4 +23,6 @@ export type SyntaxKind =
|
|
|
20
23
|
| 'ArrayLiteral'
|
|
21
24
|
| 'StringLiteralExpr'
|
|
22
25
|
| 'NumberLiteralExpr'
|
|
23
|
-
| 'BooleanLiteralExpr'
|
|
26
|
+
| 'BooleanLiteralExpr'
|
|
27
|
+
| 'ObjectLiteralExpr'
|
|
28
|
+
| 'ObjectField';
|
package/src/tokenizer.ts
CHANGED
|
@@ -86,6 +86,7 @@ function scan(source: string, pos: number): Token {
|
|
|
86
86
|
scanWhitespace(source, pos) ??
|
|
87
87
|
scanComment(source, pos) ??
|
|
88
88
|
scanAt(source, pos) ??
|
|
89
|
+
scanKeywordNumber(source, pos) ??
|
|
89
90
|
scanIdent(source, pos) ??
|
|
90
91
|
scanNumber(source, pos) ??
|
|
91
92
|
scanString(source, pos) ??
|
|
@@ -151,6 +152,22 @@ function scanIdent(source: string, pos: number): Token | undefined {
|
|
|
151
152
|
return { kind: 'Ident', text: source.slice(pos, end) };
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
const KEYWORD_NUMBERS = ['-Infinity', 'Infinity', 'NaN'] as const;
|
|
156
|
+
|
|
157
|
+
// `NaN`, `Infinity`, and `-Infinity` are numeric literals, not identifiers.
|
|
158
|
+
// They must match before `scanIdent` (which would otherwise claim the
|
|
159
|
+
// identifier-led words) and are word-bounded: a following identifier-part char
|
|
160
|
+
// (so `Infinityx` / `NaNxyz`) keeps the run an `Ident` rather than a number.
|
|
161
|
+
function scanKeywordNumber(source: string, pos: number): Token | undefined {
|
|
162
|
+
for (const word of KEYWORD_NUMBERS) {
|
|
163
|
+
if (!source.startsWith(word, pos)) continue;
|
|
164
|
+
const after = readChar(source, pos + word.length);
|
|
165
|
+
if (after !== '' && isIdentPart(after)) return undefined;
|
|
166
|
+
return { kind: 'NumberLiteral', text: word };
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
function scanNumber(source: string, pos: number): Token | undefined {
|
|
155
172
|
let end = pos;
|
|
156
173
|
if (source.charAt(end) === '-') {
|
|
@@ -195,6 +212,37 @@ function scanString(source: string, pos: number): Token | undefined {
|
|
|
195
212
|
return { kind: 'StringLiteral', text: source.slice(pos, end) };
|
|
196
213
|
}
|
|
197
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Whether a `StringLiteral` token's text is properly closed. `scanString` emits
|
|
217
|
+
* the same `StringLiteral` kind for both well-formed and unterminated strings —
|
|
218
|
+
* it stops at a newline or EOF when no closing quote is found — so callers that
|
|
219
|
+
* need to distinguish the two ask here.
|
|
220
|
+
*
|
|
221
|
+
* Because `scanString` stops at the *first* unescaped `"`, the only quote whose
|
|
222
|
+
* escaping can be in question is the **last character**: the text is terminated
|
|
223
|
+
* iff it opens with `"`, ends with `"`, and that closing `"` is not escaped.
|
|
224
|
+
* Under the `\\`-escapes-the-next-character rule, a `"` is unescaped iff an
|
|
225
|
+
* **even** number of backslashes immediately precede it — each `\\` pair cancels,
|
|
226
|
+
* and an odd run leaves the final `\` escaping the quote. So it suffices to
|
|
227
|
+
* count the trailing backslash run; no full re-scan is needed:
|
|
228
|
+
*
|
|
229
|
+
* - `"ok"` → 0 backslashes (even) → closing quote stands → terminated
|
|
230
|
+
* - `"a\"` → 1 backslash (odd) → the `"` is escaped → unterminated
|
|
231
|
+
* - `"a\\"` → 2 backslashes (even) → escaped `\`, real `"` → terminated
|
|
232
|
+
*
|
|
233
|
+
* A lone `"` (length 1) or a text with no closing `"` is unterminated.
|
|
234
|
+
*/
|
|
235
|
+
export function isTerminatedStringLiteral(text: string): boolean {
|
|
236
|
+
if (text.length < 2 || text.charAt(0) !== '"' || text.charAt(text.length - 1) !== '"') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
let backslashes = 0;
|
|
240
|
+
for (let i = text.length - 2; i >= 1 && text.charAt(i) === '\\'; i--) {
|
|
241
|
+
backslashes++;
|
|
242
|
+
}
|
|
243
|
+
return backslashes % 2 === 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
198
246
|
function scanPunctuation(source: string, pos: number): Token | undefined {
|
|
199
247
|
const kind = PUNCTUATION[source.charAt(pos)];
|
|
200
248
|
if (kind === undefined) return undefined;
|
package/dist/tokenizer.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tokenizer.mjs","names":["#source","#buffer","#pos","#scanNext"],"sources":["../src/tokenizer.ts"],"sourcesContent":["export type TokenKind =\n | 'Ident'\n | 'StringLiteral'\n | 'NumberLiteral'\n | 'At'\n | 'DoubleAt'\n | 'LBrace'\n | 'RBrace'\n | 'LParen'\n | 'RParen'\n | 'LBracket'\n | 'RBracket'\n | 'Equals'\n | 'Question'\n | 'Dot'\n | 'Comma'\n | 'Colon'\n | 'Whitespace'\n | 'Newline'\n | 'Comment'\n | 'Invalid'\n | 'Eof';\n\nexport interface Token {\n readonly kind: TokenKind;\n readonly text: string;\n}\n\nexport class Tokenizer {\n readonly #source: string;\n #pos: number;\n readonly #buffer: Token[];\n\n constructor(source: string) {\n this.#source = source;\n this.#pos = 0;\n this.#buffer = [];\n }\n\n next(): Token {\n const next = this.#buffer.shift();\n if (next) {\n return next;\n }\n return this.#scanNext();\n }\n\n peek(offset = 0): Token {\n if (offset > this.#buffer.length) {\n const last = this.#buffer.at(-1);\n if (last?.kind === 'Eof') {\n return last;\n }\n }\n\n const token = this.#buffer[offset];\n if (token) {\n return token;\n }\n\n while (this.#buffer.length <= offset) {\n const token = this.#scanNext();\n if (token.kind === 'Eof') {\n return token;\n }\n this.#buffer.push(token);\n }\n\n return this.#buffer[offset] as Token;\n }\n\n #scanNext(): Token {\n const token = scan(this.#source, this.#pos);\n this.#pos += token.text.length;\n return token;\n }\n}\n\nfunction scan(source: string, pos: number): Token {\n if (pos >= source.length) {\n return { kind: 'Eof', text: '' };\n }\n\n return (\n scanNewline(source, pos) ??\n scanWhitespace(source, pos) ??\n scanComment(source, pos) ??\n scanAt(source, pos) ??\n scanIdent(source, pos) ??\n scanNumber(source, pos) ??\n scanString(source, pos) ??\n scanPunctuation(source, pos) ?? {\n kind: 'Invalid' as const,\n text: readChar(source, pos),\n }\n );\n}\n\nfunction scanNewline(source: string, pos: number): Token | undefined {\n const ch = source.charAt(pos);\n if (ch !== '\\r' && ch !== '\\n') return undefined;\n if (ch === '\\r' && source.charAt(pos + 1) === '\\n') {\n return { kind: 'Newline', text: '\\r\\n' };\n }\n return { kind: 'Newline', text: ch };\n}\n\nfunction scanWhitespace(source: string, pos: number): Token | undefined {\n const ch = source.charAt(pos);\n if (ch !== ' ' && ch !== '\\t') return undefined;\n let end = pos + 1;\n while (end < source.length) {\n const c = source.charAt(end);\n if (c !== ' ' && c !== '\\t') break;\n end++;\n }\n return { kind: 'Whitespace', text: source.slice(pos, end) };\n}\n\nfunction scanComment(source: string, pos: number): Token | undefined {\n if (source.charAt(pos) !== '/' || source.charAt(pos + 1) !== '/') return undefined;\n let end = pos + 2;\n while (end < source.length) {\n const c = source.charAt(end);\n if (c === '\\n' || c === '\\r') break;\n end++;\n }\n return { kind: 'Comment', text: source.slice(pos, end) };\n}\n\nfunction scanAt(source: string, pos: number): Token | undefined {\n if (source.charAt(pos) !== '@') return undefined;\n if (source.charAt(pos + 1) === '@') {\n return { kind: 'DoubleAt', text: '@@' };\n }\n return { kind: 'At', text: '@' };\n}\n\nfunction scanIdent(source: string, pos: number): Token | undefined {\n const ch = readChar(source, pos);\n if (!isIdentStart(ch)) return undefined;\n let end = pos + ch.length;\n while (end < source.length) {\n const c = readChar(source, end);\n if (isIdentPart(c)) {\n end += c.length;\n } else {\n break;\n }\n }\n return { kind: 'Ident', text: source.slice(pos, end) };\n}\n\nfunction scanNumber(source: string, pos: number): Token | undefined {\n let end = pos;\n if (source.charAt(end) === '-') {\n if (end + 1 >= source.length || !isDigit(source.charAt(end + 1))) return undefined;\n end++;\n } else if (!isDigit(source.charAt(end))) {\n return undefined;\n }\n end++;\n while (end < source.length && isDigit(source.charAt(end))) {\n end++;\n }\n if (source.charAt(end) === '.' && end + 1 < source.length && isDigit(source.charAt(end + 1))) {\n end++; // consume the dot\n while (end < source.length && isDigit(source.charAt(end))) {\n end++;\n }\n }\n return { kind: 'NumberLiteral', text: source.slice(pos, end) };\n}\n\nfunction scanString(source: string, pos: number): Token | undefined {\n if (source.charAt(pos) !== '\"') return undefined;\n let end = pos + 1;\n while (end < source.length) {\n const c = source.charAt(end);\n if (c === '\\\\' && end + 1 < source.length) {\n end += 2; // skip escape sequence\n continue;\n }\n if (c === '\"') {\n end++; // include closing quote\n return { kind: 'StringLiteral', text: source.slice(pos, end) };\n }\n if (c === '\\n' || c === '\\r') {\n // Unterminated: stop before newline\n return { kind: 'StringLiteral', text: source.slice(pos, end) };\n }\n end++;\n }\n // Unterminated at EOF\n return { kind: 'StringLiteral', text: source.slice(pos, end) };\n}\n\nfunction scanPunctuation(source: string, pos: number): Token | undefined {\n const kind = PUNCTUATION[source.charAt(pos)];\n if (kind === undefined) return undefined;\n return { kind, text: source.charAt(pos) };\n}\n\nfunction readChar(source: string, pos: number): string {\n const cp = source.codePointAt(pos);\n return cp !== undefined ? String.fromCodePoint(cp) : '';\n}\n\nfunction isIdentStart(ch: string): boolean {\n return /\\p{L}/u.test(ch) || ch === '_';\n}\n\nfunction isIdentPart(ch: string): boolean {\n return isIdentStart(ch) || isDigit(ch) || ch === '-';\n}\n\nfunction isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9';\n}\n\nconst PUNCTUATION: Record<string, TokenKind> = {\n '{': 'LBrace',\n '}': 'RBrace',\n '(': 'LParen',\n ')': 'RParen',\n '[': 'LBracket',\n ']': 'RBracket',\n '=': 'Equals',\n '?': 'Question',\n '.': 'Dot',\n ',': 'Comma',\n ':': 'Colon',\n};\n"],"mappings":"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"}
|