@ldclabs/kip-lang 0.4.0 → 2.0.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +89 -64
  4. package/dist/ast.d.ts +511 -145
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/diagnostics.d.ts +8 -2
  7. package/dist/diagnostics.d.ts.map +1 -1
  8. package/dist/diagnostics.js +32 -3
  9. package/dist/diagnostics.js.map +1 -1
  10. package/dist/exec-ast.d.ts +514 -149
  11. package/dist/exec-ast.d.ts.map +1 -1
  12. package/dist/exec-ast.js +8 -7
  13. package/dist/exec-ast.js.map +1 -1
  14. package/dist/formatter.d.ts.map +1 -1
  15. package/dist/formatter.js +870 -479
  16. package/dist/formatter.js.map +1 -1
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lexer.d.ts.map +1 -1
  22. package/dist/lexer.js +12 -30
  23. package/dist/lexer.js.map +1 -1
  24. package/dist/lower.d.ts +1 -2
  25. package/dist/lower.d.ts.map +1 -1
  26. package/dist/lower.js +1287 -598
  27. package/dist/lower.js.map +1 -1
  28. package/dist/parser.d.ts.map +1 -1
  29. package/dist/parser.js +2410 -1300
  30. package/dist/parser.js.map +1 -1
  31. package/dist/semantics.d.ts +11 -7
  32. package/dist/semantics.d.ts.map +1 -1
  33. package/dist/semantics.js +295 -180
  34. package/dist/semantics.js.map +1 -1
  35. package/dist/token.d.ts +130 -40
  36. package/dist/token.d.ts.map +1 -1
  37. package/dist/token.js +264 -83
  38. package/dist/token.js.map +1 -1
  39. package/dist/version.d.ts +2 -2
  40. package/dist/version.js +2 -2
  41. package/package.json +35 -5
  42. package/src/ast.ts +914 -0
  43. package/src/budget.ts +108 -0
  44. package/src/diagnostics.ts +182 -0
  45. package/src/errors.ts +42 -0
  46. package/src/exec-ast.ts +614 -0
  47. package/src/formatter.ts +1339 -0
  48. package/src/index.ts +226 -0
  49. package/src/lexer.ts +459 -0
  50. package/src/lower.ts +2011 -0
  51. package/src/parser.ts +3506 -0
  52. package/src/semantics.ts +392 -0
  53. package/src/token.ts +408 -0
  54. package/src/version.ts +13 -0
package/src/index.ts ADDED
@@ -0,0 +1,226 @@
1
+ export { tokenize } from './lexer.js'
2
+ export { parse } from './parser.js'
3
+ export type { ParseResult } from './parser.js'
4
+ export { format } from './formatter.js'
5
+ export type { FormatOptions } from './formatter.js'
6
+ export { diagnose, validateExecutable } from './diagnostics.js'
7
+ export type { Diagnostic } from './diagnostics.js'
8
+ export { analyzeSemantics } from './semantics.js'
9
+
10
+ export {
11
+ TokenType,
12
+ KEYWORDS,
13
+ FUNCTIONS,
14
+ AGGREGATES,
15
+ isKeyword,
16
+ isFunction,
17
+ isAggregate,
18
+ isLiteral,
19
+ isTrivia,
20
+ isIdentifierLike
21
+ } from './token.js'
22
+ export type { Token, Position, Range } from './token.js'
23
+
24
+ export type {
25
+ // Root
26
+ Program,
27
+ Statement,
28
+ KqlStatement,
29
+ KmlStatement,
30
+ MetaStatement,
31
+ MutationClause,
32
+ // Shared operands
33
+ SchemaSymbol,
34
+ ScalarValue,
35
+ TargetRef,
36
+ Handle,
37
+ // KQL
38
+ FindStatement,
39
+ AsOfClause,
40
+ ForTimeClause,
41
+ EpistemicClause,
42
+ OrderByClause,
43
+ OrderItem,
44
+ LimitClause,
45
+ CursorClause,
46
+ WhereClause,
47
+ WherePattern,
48
+ ConceptPattern,
49
+ PropositionPattern,
50
+ AssertionPattern,
51
+ EvidencePattern,
52
+ ActivityPattern,
53
+ StructuralPattern,
54
+ BeliefPattern,
55
+ BeliefSlotPattern,
56
+ FilterClause,
57
+ NotClause,
58
+ OptionalClause,
59
+ UnionClause,
60
+ PropositionTuple,
61
+ Term,
62
+ PredicateAtom,
63
+ RawPredicateExpression,
64
+ PredicatePathAtom,
65
+ PathQuantifier,
66
+ ObjectPattern,
67
+ // KML
68
+ MutateStatement,
69
+ CreateConceptStatement,
70
+ UpsertConceptStatement,
71
+ EnsurePropositionStatement,
72
+ AssertStatement,
73
+ CreateEvidenceStatement,
74
+ CreateAssertionStatement,
75
+ CreateActivityStatement,
76
+ TypeClause,
77
+ ClientKeyClause,
78
+ NameClause,
79
+ MatchClause,
80
+ SetFieldsClause,
81
+ SetAttributesClause,
82
+ SetFacetClause,
83
+ UnsetAttributesClause,
84
+ UnsetFacetClause,
85
+ UnsetField,
86
+ SetStructuralClause,
87
+ StructuralAssignment,
88
+ UnsetStructuralClause,
89
+ StructuralRemoval,
90
+ ExpectVersionClause,
91
+ ExpectStateClause,
92
+ UpdateStatement,
93
+ UpdateAction,
94
+ RetractAssertionStatement,
95
+ SupersedeAssertionStatement,
96
+ CorrectEvidenceStatement,
97
+ TransitionActivityStatement,
98
+ SetRetentionStatement,
99
+ ArchiveStatement,
100
+ TombstoneStatement,
101
+ PurgeStatement,
102
+ MergeConceptStatement,
103
+ // META
104
+ DescribeStatement,
105
+ DescribeTargetKind,
106
+ ListStatement,
107
+ ListTargetKind,
108
+ SearchStatement,
109
+ SearchKind,
110
+ VerifyStatement,
111
+ VerifyTargetKind,
112
+ ValidateStatement,
113
+ ValidateTargetKind,
114
+ PreviewStatement,
115
+ HistoryStatement,
116
+ ChangesStatement,
117
+ SnapshotStatement,
118
+ ExportCapsuleStatement,
119
+ // Expressions
120
+ Expression,
121
+ BinaryExpression,
122
+ UnaryExpression,
123
+ FunctionCallExpr,
124
+ AggregateExpr,
125
+ FieldAccess,
126
+ FieldStep,
127
+ DotStep,
128
+ IndexStep,
129
+ VariableRef,
130
+ ParameterRef,
131
+ StringLiteral,
132
+ NumberLiteral,
133
+ BooleanLiteral,
134
+ NullLiteral,
135
+ ObjectEntry,
136
+ ObjectLiteral,
137
+ ArrayLiteral
138
+ } from './ast.js'
139
+
140
+ export { lower, lowerAll, lowerStatement } from './lower.js'
141
+ export {
142
+ checkBudget,
143
+ checkBatchBudget,
144
+ MAX_KIP_INPUT_LEN,
145
+ MAX_KIP_NESTING_DEPTH,
146
+ MAX_KIP_BATCH_COMMANDS
147
+ } from './budget.js'
148
+ export { KipSyntaxError, invalidSyntax, resourceExhausted } from './errors.js'
149
+ export type { KipSyntaxCode } from './errors.js'
150
+
151
+ export type {
152
+ // Root
153
+ Command,
154
+ BoundValue,
155
+ KipValue,
156
+ Scalar,
157
+ SymbolRef,
158
+ ElementRef,
159
+ // Shared terms
160
+ DotPathVar,
161
+ PathStep,
162
+ PredAtom,
163
+ PredTerm,
164
+ PredPathAtom,
165
+ HopRange,
166
+ Term as ExecTerm,
167
+ ObjectMatcher,
168
+ MatchValue,
169
+ PropositionMatcher,
170
+ // KQL
171
+ KqlQuery,
172
+ FindClause,
173
+ FindExpression,
174
+ AggregationFunction,
175
+ AsOf,
176
+ OrderByItem,
177
+ OrderDirection,
178
+ WhereClause as ExecWhereClause,
179
+ BeliefTarget,
180
+ FilterExpression,
181
+ FilterOperand,
182
+ FilterFunction,
183
+ ComparisonOperator,
184
+ LogicalOperator,
185
+ // KML
186
+ KmlStatement as ExecKmlStatement,
187
+ MutationClause as ExecMutationClause,
188
+ ConceptCreate,
189
+ ConceptUpsert,
190
+ RecordCreate,
191
+ EnsureProposition,
192
+ FacetAssignment,
193
+ FacetUnset,
194
+ StructuralEdge,
195
+ StructuralRemoval as ExecStructuralRemoval,
196
+ Assignments,
197
+ MutationValue,
198
+ UpdateExpr,
199
+ UpdateFunction,
200
+ UpdateStatement as ExecUpdateStatement,
201
+ UpdateAction as ExecUpdateAction,
202
+ RetractAssertion,
203
+ SupersedeAssertion,
204
+ CorrectEvidence,
205
+ TransitionActivity,
206
+ SetRetention,
207
+ RemovalStatement,
208
+ PurgeStatement as ExecPurgeStatement,
209
+ MergeConcept,
210
+ // META
211
+ MetaCommand,
212
+ DescribeTarget,
213
+ ListCommand,
214
+ ListTarget,
215
+ SearchCommand,
216
+ SearchTarget,
217
+ VerifyTarget,
218
+ ValidateCommand,
219
+ ValidateTarget,
220
+ PreviewCommand,
221
+ HistoryCommand,
222
+ ChangesCommand,
223
+ ExportCapsuleCommand
224
+ } from './exec-ast.js'
225
+
226
+ export { PARSER_VERSION, KIP_SPEC_REVISION } from './version.js'
package/src/lexer.ts ADDED
@@ -0,0 +1,459 @@
1
+ import { Token, TokenType, KEYWORDS } from './token.js'
2
+
3
+ /**
4
+ * Characters that continue a word for boundary purposes. Unicode-aware on
5
+ * purpose: `FROMé` is one word to the grammar even though `é` cannot start a
6
+ * KIP identifier, so `FROM` must not be lifted out of it as a keyword.
7
+ */
8
+ const WORD_CHAR = /[\p{Alphabetic}\p{N}]/u
9
+
10
+ export function tokenize(source: string): Token[] {
11
+ const lexer = new Lexer(source)
12
+ return lexer.tokenizeAll()
13
+ }
14
+
15
+ class Lexer {
16
+ private source: string
17
+ private pos: number = 0
18
+ private line: number = 0
19
+ private column: number = 0
20
+ private tokens: Token[] = []
21
+
22
+ constructor(source: string) {
23
+ this.source = source
24
+ }
25
+
26
+ tokenizeAll(): Token[] {
27
+ while (this.pos < this.source.length) {
28
+ this.scanToken()
29
+ }
30
+ this.tokens.push({
31
+ type: TokenType.EOF,
32
+ value: '',
33
+ offset: this.pos,
34
+ line: this.line,
35
+ column: this.column
36
+ })
37
+ return this.tokens
38
+ }
39
+
40
+ private peek(): string {
41
+ return this.source[this.pos] ?? ''
42
+ }
43
+
44
+ private peekAt(offset: number): string {
45
+ return this.source[this.pos + offset] ?? ''
46
+ }
47
+
48
+ private advance(): string {
49
+ const ch = this.source[this.pos]!
50
+ this.pos++
51
+ if (ch === '\n') {
52
+ this.line++
53
+ this.column = 0
54
+ } else {
55
+ this.column++
56
+ }
57
+ return ch
58
+ }
59
+
60
+ private scanToken(): void {
61
+ const ch = this.peek()
62
+
63
+ // Whitespace (excluding newlines)
64
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
65
+ this.scanWhitespace()
66
+ return
67
+ }
68
+
69
+ // Newlines
70
+ if (ch === '\n') {
71
+ const start = this.pos
72
+ const startLine = this.line
73
+ const startCol = this.column
74
+ this.advance()
75
+ this.pushToken(TokenType.Newline, '\n', start, startLine, startCol)
76
+ return
77
+ }
78
+
79
+ // Comments
80
+ if (ch === '/' && this.peekAt(1) === '/') {
81
+ this.scanComment()
82
+ return
83
+ }
84
+
85
+ // Strings
86
+ if (ch === '"') {
87
+ this.scanString()
88
+ return
89
+ }
90
+
91
+ // Numbers: digits or negative sign followed by digit
92
+ if (this.isDigit(ch) || (ch === '-' && this.isDigit(this.peekAt(1)))) {
93
+ this.scanNumber()
94
+ return
95
+ }
96
+
97
+ // Variables: ?identifier
98
+ if (ch === '?') {
99
+ this.scanVariable()
100
+ return
101
+ }
102
+
103
+ // Identifiers and keywords
104
+ if (this.isIdentStart(ch)) {
105
+ this.scanIdentifier()
106
+ return
107
+ }
108
+
109
+ // Two-character operators
110
+ if (ch === '=' && this.peekAt(1) === '=') {
111
+ this.scanFixedToken(TokenType.Eq, 2)
112
+ return
113
+ }
114
+ if (ch === '!' && this.peekAt(1) === '=') {
115
+ this.scanFixedToken(TokenType.NotEq, 2)
116
+ return
117
+ }
118
+ if (ch === '<' && this.peekAt(1) === '=') {
119
+ this.scanFixedToken(TokenType.LtEq, 2)
120
+ return
121
+ }
122
+ if (ch === '>' && this.peekAt(1) === '=') {
123
+ this.scanFixedToken(TokenType.GtEq, 2)
124
+ return
125
+ }
126
+ if (ch === '&' && this.peekAt(1) === '&') {
127
+ this.scanFixedToken(TokenType.And, 2)
128
+ return
129
+ }
130
+ if (ch === '|' && this.peekAt(1) === '|') {
131
+ this.scanFixedToken(TokenType.Or, 2)
132
+ return
133
+ }
134
+
135
+ // Single-character operators. A `-` that did not start a number is the
136
+ // unary negation of `unary_expression`, e.g. `FILTER(-?x < 0)`.
137
+ if (ch === '-') {
138
+ this.scanFixedToken(TokenType.Minus, 1)
139
+ return
140
+ }
141
+ if (ch === '<') {
142
+ this.scanFixedToken(TokenType.Lt, 1)
143
+ return
144
+ }
145
+ if (ch === '>') {
146
+ this.scanFixedToken(TokenType.Gt, 1)
147
+ return
148
+ }
149
+ if (ch === '!') {
150
+ this.scanFixedToken(TokenType.Bang, 1)
151
+ return
152
+ }
153
+
154
+ // Colon: could be parameter placeholder (:name) or punctuation
155
+ if (ch === ':') {
156
+ if (this.isIdentStart(this.peekAt(1))) {
157
+ this.scanParameter()
158
+ return
159
+ }
160
+ this.scanFixedToken(TokenType.Colon, 1)
161
+ return
162
+ }
163
+
164
+ // Pipe: could be || (already handled) or single |
165
+ if (ch === '|') {
166
+ this.scanFixedToken(TokenType.Pipe, 1)
167
+ return
168
+ }
169
+
170
+ // Punctuation
171
+ const punctMap: Record<string, TokenType> = {
172
+ '{': TokenType.LBrace,
173
+ '}': TokenType.RBrace,
174
+ '(': TokenType.LParen,
175
+ ')': TokenType.RParen,
176
+ '[': TokenType.LBracket,
177
+ ']': TokenType.RBracket,
178
+ ',': TokenType.Comma,
179
+ '.': TokenType.Dot
180
+ }
181
+
182
+ if (ch in punctMap) {
183
+ this.scanFixedToken(punctMap[ch]!, 1)
184
+ return
185
+ }
186
+
187
+ // Unknown character
188
+ const start = this.pos
189
+ const startLine = this.line
190
+ const startCol = this.column
191
+ this.advance()
192
+ this.pushToken(TokenType.Unknown, ch, start, startLine, startCol)
193
+ }
194
+
195
+ private scanWhitespace(): void {
196
+ const start = this.pos
197
+ const startLine = this.line
198
+ const startCol = this.column
199
+ while (this.pos < this.source.length) {
200
+ const ch = this.peek()
201
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
202
+ this.advance()
203
+ } else {
204
+ break
205
+ }
206
+ }
207
+ this.pushToken(
208
+ TokenType.Whitespace,
209
+ this.source.slice(start, this.pos),
210
+ start,
211
+ startLine,
212
+ startCol
213
+ )
214
+ }
215
+
216
+ private scanComment(): void {
217
+ const start = this.pos
218
+ const startLine = this.line
219
+ const startCol = this.column
220
+ // Skip //
221
+ this.advance()
222
+ this.advance()
223
+ while (this.pos < this.source.length && this.peek() !== '\n') {
224
+ this.advance()
225
+ }
226
+ this.pushToken(
227
+ TokenType.Comment,
228
+ this.source.slice(start, this.pos),
229
+ start,
230
+ startLine,
231
+ startCol
232
+ )
233
+ }
234
+
235
+ private scanString(): void {
236
+ const start = this.pos
237
+ const startLine = this.line
238
+ const startCol = this.column
239
+ this.advance() // skip opening "
240
+ while (this.pos < this.source.length) {
241
+ const ch = this.peek()
242
+ if (ch === '\\') {
243
+ this.advance() // skip backslash
244
+ if (this.pos < this.source.length) {
245
+ this.advance() // skip escaped char
246
+ }
247
+ } else if (ch === '"') {
248
+ this.advance() // skip closing "
249
+ this.pushToken(
250
+ TokenType.String,
251
+ this.source.slice(start, this.pos),
252
+ start,
253
+ startLine,
254
+ startCol
255
+ )
256
+ return
257
+ } else if (ch === '\n') {
258
+ // Unterminated string at newline
259
+ break
260
+ } else {
261
+ this.advance()
262
+ }
263
+ }
264
+ // Unterminated string
265
+ this.pushToken(
266
+ TokenType.String,
267
+ this.source.slice(start, this.pos),
268
+ start,
269
+ startLine,
270
+ startCol
271
+ )
272
+ }
273
+
274
+ private scanNumber(): void {
275
+ const start = this.pos
276
+ const startLine = this.line
277
+ const startCol = this.column
278
+
279
+ // Optional negative sign
280
+ if (this.peek() === '-') {
281
+ this.advance()
282
+ }
283
+
284
+ // Integer part
285
+ while (this.pos < this.source.length && this.isDigit(this.peek())) {
286
+ this.advance()
287
+ }
288
+
289
+ // Fractional part
290
+ if (this.peek() === '.' && this.isDigit(this.peekAt(1))) {
291
+ this.advance() // skip .
292
+ while (this.pos < this.source.length && this.isDigit(this.peek())) {
293
+ this.advance()
294
+ }
295
+ }
296
+
297
+ // Exponent part: consume only if it is complete JSON-number syntax.
298
+ if (
299
+ (this.peek() === 'e' || this.peek() === 'E') &&
300
+ (this.isDigit(this.peekAt(1)) ||
301
+ ((this.peekAt(1) === '+' || this.peekAt(1) === '-') &&
302
+ this.isDigit(this.peekAt(2))))
303
+ ) {
304
+ this.advance()
305
+ if (this.peek() === '+' || this.peek() === '-') {
306
+ this.advance()
307
+ }
308
+ while (this.pos < this.source.length && this.isDigit(this.peek())) {
309
+ this.advance()
310
+ }
311
+ }
312
+
313
+ this.pushToken(
314
+ TokenType.Number,
315
+ this.source.slice(start, this.pos),
316
+ start,
317
+ startLine,
318
+ startCol
319
+ )
320
+ }
321
+
322
+ private scanVariable(): void {
323
+ const start = this.pos
324
+ const startLine = this.line
325
+ const startCol = this.column
326
+ this.advance() // skip ?
327
+
328
+ if (this.isIdentStart(this.peek())) {
329
+ while (this.pos < this.source.length && this.isIdentPart(this.peek())) {
330
+ this.advance()
331
+ }
332
+ this.pushToken(
333
+ TokenType.Variable,
334
+ this.source.slice(start, this.pos),
335
+ start,
336
+ startLine,
337
+ startCol
338
+ )
339
+ } else {
340
+ // Bare ? is unknown
341
+ this.pushToken(TokenType.Unknown, '?', start, startLine, startCol)
342
+ }
343
+ }
344
+
345
+ private scanParameter(): void {
346
+ const start = this.pos
347
+ const startLine = this.line
348
+ const startCol = this.column
349
+ this.advance() // skip :
350
+
351
+ while (this.pos < this.source.length && this.isIdentPart(this.peek())) {
352
+ this.advance()
353
+ }
354
+ this.pushToken(
355
+ TokenType.Parameter,
356
+ this.source.slice(start, this.pos),
357
+ start,
358
+ startLine,
359
+ startCol
360
+ )
361
+ }
362
+
363
+ private scanIdentifier(): void {
364
+ const start = this.pos
365
+ const startLine = this.line
366
+ const startCol = this.column
367
+
368
+ while (this.pos < this.source.length && this.isIdentPart(this.peek())) {
369
+ this.advance()
370
+ }
371
+
372
+ const value = this.source.slice(start, this.pos)
373
+ const upper = value.toUpperCase()
374
+
375
+ // Check for boolean and null literals (case-sensitive)
376
+ if (value === 'true' || value === 'false') {
377
+ this.pushToken(TokenType.Boolean, value, start, startLine, startCol)
378
+ return
379
+ }
380
+ if (value === 'null') {
381
+ this.pushToken(TokenType.Null, value, start, startLine, startCol)
382
+ return
383
+ }
384
+
385
+ // A keyword only counts as one at a word boundary. Letters, digits and `_`
386
+ // are already absorbed above, so what remains to exclude is `?` and `"`:
387
+ // `FROM?x` and `TYPE"Drug"` are not `FROM ?x` and `TYPE "Drug"` written
388
+ // tersely, they are unparseable — and accepting them here would let a
389
+ // command run on this parser that a spec-conformant engine rejects.
390
+ //
391
+ // KIP 2.0 keywords are ASCII case-insensitive, so `find`, `Find` and
392
+ // `FIND` are one token; the original spelling is kept in `value` so the
393
+ // formatter can canonicalize it to uppercase without losing the source.
394
+ if (this.atWordBoundary()) {
395
+ const kwType = KEYWORDS.get(upper)
396
+ if (kwType) {
397
+ this.pushToken(kwType, value, start, startLine, startCol)
398
+ return
399
+ }
400
+ }
401
+
402
+ this.pushToken(TokenType.Identifier, value, start, startLine, startCol)
403
+ }
404
+
405
+ private scanFixedToken(type: TokenType, length: number): void {
406
+ const start = this.pos
407
+ const startLine = this.line
408
+ const startCol = this.column
409
+ for (let i = 0; i < length; i++) {
410
+ this.advance()
411
+ }
412
+ this.pushToken(
413
+ type,
414
+ this.source.slice(start, this.pos),
415
+ start,
416
+ startLine,
417
+ startCol
418
+ )
419
+ }
420
+
421
+ private pushToken(
422
+ type: TokenType,
423
+ value: string,
424
+ offset: number,
425
+ line: number,
426
+ column: number
427
+ ): void {
428
+ this.tokens.push({ type, value, offset, line, column })
429
+ }
430
+
431
+ /**
432
+ * True when the character at the cursor ends a keyword.
433
+ *
434
+ * Mirrors the grammar's word boundary: a keyword may butt directly against
435
+ * punctuation (`WHERE{`, `FIND(`) but not against something that could have
436
+ * been part of an identifier, a variable, or a string.
437
+ */
438
+ private atWordBoundary(): boolean {
439
+ if (this.pos >= this.source.length) return true
440
+ // Read a whole code point: a surrogate pair tested one unit at a time is
441
+ // alphabetic in neither half, which would read `FROM𝔸` as `FROM` + junk
442
+ // while a `char`-based engine reads it as one word.
443
+ const cp = String.fromCodePoint(this.source.codePointAt(this.pos)!)
444
+ if (cp === '?' || cp === '"' || cp === '_') return false
445
+ return !WORD_CHAR.test(cp)
446
+ }
447
+
448
+ private isDigit(ch: string): boolean {
449
+ return ch >= '0' && ch <= '9'
450
+ }
451
+
452
+ private isIdentStart(ch: string): boolean {
453
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_'
454
+ }
455
+
456
+ private isIdentPart(ch: string): boolean {
457
+ return this.isIdentStart(ch) || this.isDigit(ch)
458
+ }
459
+ }