@ldclabs/kip-lang 0.3.1 → 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.
- package/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +133 -44
- package/dist/ast.d.ts +519 -144
- package/dist/ast.d.ts.map +1 -1
- package/dist/budget.d.ts +37 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +105 -0
- package/dist/budget.js.map +1 -0
- package/dist/diagnostics.d.ts +8 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +32 -3
- package/dist/diagnostics.js.map +1 -1
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/exec-ast.d.ts +679 -0
- package/dist/exec-ast.d.ts.map +1 -0
- package/dist/exec-ast.js +24 -0
- package/dist/exec-ast.js.map +1 -0
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +870 -479
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +46 -33
- package/dist/lexer.js.map +1 -1
- package/dist/lower.d.ts +17 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +1566 -0
- package/dist/lower.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +2476 -1152
- package/dist/parser.js.map +1 -1
- package/dist/semantics.d.ts +11 -7
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +295 -180
- package/dist/semantics.js.map +1 -1
- package/dist/token.d.ts +130 -40
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +264 -83
- package/dist/token.js.map +1 -1
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +36 -6
- package/src/ast.ts +914 -0
- package/src/budget.ts +108 -0
- package/src/diagnostics.ts +182 -0
- package/src/errors.ts +42 -0
- package/src/exec-ast.ts +614 -0
- package/src/formatter.ts +1339 -0
- package/src/index.ts +226 -0
- package/src/lexer.ts +459 -0
- package/src/lower.ts +2011 -0
- package/src/parser.ts +3506 -0
- package/src/semantics.ts +392 -0
- package/src/token.ts +408 -0
- 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
|
+
}
|