@jbrowse/jexl 3.0.2 → 3.2.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/README.md +26 -2
- package/dist/Expression.d.ts +19 -7
- package/dist/Expression.js +32 -11
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +13 -12
- package/dist/Jexl.js +31 -30
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +9 -9
- package/dist/Lexer.js +61 -24
- package/dist/Lexer.js.map +1 -1
- package/dist/evaluator/Evaluator.d.ts +20 -34
- package/dist/evaluator/Evaluator.js +19 -64
- package/dist/evaluator/Evaluator.js.map +1 -1
- package/dist/evaluator/compile.d.ts +27 -0
- package/dist/evaluator/compile.js +279 -0
- package/dist/evaluator/compile.js.map +1 -0
- package/dist/grammar.d.ts +29 -9
- package/dist/grammar.js +23 -7
- package/dist/grammar.js.map +1 -1
- package/dist/parser/Parser.d.ts +7 -8
- package/dist/parser/Parser.js +46 -14
- package/dist/parser/Parser.js.map +1 -1
- package/dist/parser/handlers.d.ts +8 -8
- package/dist/parser/handlers.js +84 -51
- package/dist/parser/handlers.js.map +1 -1
- package/dist/parser/states.d.ts +2 -2
- package/dist/parser/states.js +19 -40
- package/dist/parser/states.js.map +1 -1
- package/dist/types.d.ts +18 -7
- package/esm/Expression.d.ts +19 -7
- package/esm/Expression.js +32 -11
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +13 -12
- package/esm/Jexl.js +31 -30
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +9 -9
- package/esm/Lexer.js +61 -24
- package/esm/Lexer.js.map +1 -1
- package/esm/evaluator/Evaluator.d.ts +20 -34
- package/esm/evaluator/Evaluator.js +19 -31
- package/esm/evaluator/Evaluator.js.map +1 -1
- package/esm/evaluator/compile.d.ts +27 -0
- package/esm/evaluator/compile.js +276 -0
- package/esm/evaluator/compile.js.map +1 -0
- package/esm/grammar.d.ts +29 -9
- package/esm/grammar.js +22 -7
- package/esm/grammar.js.map +1 -1
- package/esm/parser/Parser.d.ts +7 -8
- package/esm/parser/Parser.js +46 -14
- package/esm/parser/Parser.js.map +1 -1
- package/esm/parser/handlers.d.ts +8 -8
- package/esm/parser/handlers.js +81 -50
- package/esm/parser/handlers.js.map +1 -1
- package/esm/parser/states.d.ts +2 -2
- package/esm/parser/states.js +19 -40
- package/esm/parser/states.js.map +1 -1
- package/esm/types.d.ts +18 -7
- package/package.json +25 -28
- package/src/Expression.ts +31 -17
- package/src/Jexl.ts +59 -21
- package/src/Lexer.ts +63 -33
- package/src/evaluator/Evaluator.ts +27 -44
- package/src/evaluator/compile.ts +317 -0
- package/src/grammar.ts +69 -21
- package/src/parser/Parser.ts +50 -28
- package/src/parser/handlers.ts +122 -76
- package/src/parser/states.ts +22 -42
- package/src/types.ts +22 -7
- package/CHANGELOG.md +0 -249
- package/dist/evaluator/handlers.d.ts +0 -112
- package/dist/evaluator/handlers.js +0 -208
- package/dist/evaluator/handlers.js.map +0 -1
- package/esm/evaluator/handlers.d.ts +0 -112
- package/esm/evaluator/handlers.js +0 -194
- package/esm/evaluator/handlers.js.map +0 -1
- package/src/evaluator/handlers.ts +0 -212
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Grammar } from '../grammar.ts'
|
|
7
|
+
import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
|
|
8
|
+
import type Evaluator from './Evaluator.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An AST node lowered to a closure. Calling it with an Evaluator (which carries
|
|
12
|
+
* the context and grammar) produces the node's value.
|
|
13
|
+
*/
|
|
14
|
+
export type CompiledNode = (ev: Evaluator) => JexlValue
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes a key that a plain assignment would mishandle. Storing to
|
|
18
|
+
* "__proto__" invokes the prototype setter rather than creating a property, so
|
|
19
|
+
* an object literal or assignment using that key silently lost its value (and
|
|
20
|
+
* re-pointed the target's prototype). Defining the property instead matches
|
|
21
|
+
* what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
|
|
22
|
+
*/
|
|
23
|
+
function defineOwn(
|
|
24
|
+
target: Record<string, JexlValue>,
|
|
25
|
+
key: string,
|
|
26
|
+
value: JexlValue
|
|
27
|
+
) {
|
|
28
|
+
Object.defineProperty(target, key, {
|
|
29
|
+
value,
|
|
30
|
+
writable: true,
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assignOwn(
|
|
37
|
+
target: Record<string, JexlValue>,
|
|
38
|
+
key: string,
|
|
39
|
+
value: JexlValue
|
|
40
|
+
) {
|
|
41
|
+
target[key] = value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Renders an interpolated value for a template literal. */
|
|
45
|
+
function stringify(value: JexlValue) {
|
|
46
|
+
if (value == null) {
|
|
47
|
+
return ''
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
typeof value === 'string' ||
|
|
51
|
+
typeof value === 'number' ||
|
|
52
|
+
typeof value === 'boolean'
|
|
53
|
+
) {
|
|
54
|
+
return String(value)
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lowers an expression tree into a tree of closures, resolving each node's type
|
|
61
|
+
* and its operator's implementation once, at compile time, rather than
|
|
62
|
+
* re-dispatching on `node.type` for every node on every evaluation. An
|
|
63
|
+
* expression compiled once and evaluated per-item — the usual shape for
|
|
64
|
+
* per-feature config callbacks — pays the dispatch cost once instead of N
|
|
65
|
+
* times.
|
|
66
|
+
*
|
|
67
|
+
* Operators are bound here, so a grammar change after compilation requires
|
|
68
|
+
* recompiling, which is what {@link Expression#compile} already documents.
|
|
69
|
+
* Functions are looked up per call, since they are commonly registered after
|
|
70
|
+
* an expression has been compiled.
|
|
71
|
+
*
|
|
72
|
+
* @param {{}} ast An expression tree, as produced by the Parser
|
|
73
|
+
* @param {{}} grammar The grammar to resolve operators against
|
|
74
|
+
* @returns {function} a closure returning the expression's value
|
|
75
|
+
* @throws {Error} if the tree contains an unrecognized node type
|
|
76
|
+
*/
|
|
77
|
+
export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
78
|
+
// AstNode types its `type` as a plain string so the Parser can build the tree
|
|
79
|
+
// loosely; narrowing to the union once, here, lets every case below see its
|
|
80
|
+
// own node type instead of repeating the same cast in each branch
|
|
81
|
+
const node = ast as AstNodeUnion
|
|
82
|
+
switch (node.type) {
|
|
83
|
+
case 'Literal': {
|
|
84
|
+
const { value } = node
|
|
85
|
+
return () => value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'Identifier': {
|
|
89
|
+
const name = node.value
|
|
90
|
+
if (!node.from) {
|
|
91
|
+
return node.relative
|
|
92
|
+
? (ev) => ev._relContext[name]
|
|
93
|
+
: (ev) => ev._context[name]
|
|
94
|
+
}
|
|
95
|
+
const from = compileAst(node.from, grammar)
|
|
96
|
+
return (ev) => {
|
|
97
|
+
const context = from(ev)
|
|
98
|
+
if (context == null) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
// an identifier chained off an array reads through its first element
|
|
102
|
+
const ctx = Array.isArray(context) ? context[0] : context
|
|
103
|
+
return ctx == null
|
|
104
|
+
? undefined
|
|
105
|
+
: (ctx as Record<string, JexlValue>)[name]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'BinaryExpression': {
|
|
110
|
+
const op = grammar.elements[node.operator]
|
|
111
|
+
if (op?.type !== 'binaryOp') {
|
|
112
|
+
// matches the tree-walking behaviour: an unknown operator yields
|
|
113
|
+
// undefined without evaluating either operand
|
|
114
|
+
return () => undefined
|
|
115
|
+
}
|
|
116
|
+
const left = compileAst(node.left, grammar)
|
|
117
|
+
const right = compileAst(node.right!, grammar)
|
|
118
|
+
const { evalOnDemand } = op
|
|
119
|
+
if (evalOnDemand) {
|
|
120
|
+
// operands stay unevaluated behind an `eval` thunk, so operators such
|
|
121
|
+
// as && and || can short-circuit
|
|
122
|
+
return (ev) =>
|
|
123
|
+
evalOnDemand({ eval: () => left(ev) }, { eval: () => right(ev) })
|
|
124
|
+
}
|
|
125
|
+
const fn = op.eval
|
|
126
|
+
if (!fn) {
|
|
127
|
+
return (ev) => {
|
|
128
|
+
left(ev)
|
|
129
|
+
right(ev)
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return (ev) => fn(left(ev), right(ev))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'UnaryExpression': {
|
|
137
|
+
const right = compileAst(node.right!, grammar)
|
|
138
|
+
const elem = grammar.elements[node.operator]
|
|
139
|
+
const fn =
|
|
140
|
+
elem?.type === 'unaryOp'
|
|
141
|
+
? elem.eval
|
|
142
|
+
: elem?.type === 'binaryOp'
|
|
143
|
+
? elem.unaryEval
|
|
144
|
+
: undefined
|
|
145
|
+
if (!fn) {
|
|
146
|
+
// the operand is still evaluated for an unknown operator, as before
|
|
147
|
+
return (ev) => {
|
|
148
|
+
right(ev)
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return (ev) => fn(right(ev))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'ConditionalExpression': {
|
|
156
|
+
const test = compileAst(node.test, grammar)
|
|
157
|
+
const consequent = node.consequent
|
|
158
|
+
? compileAst(node.consequent, grammar)
|
|
159
|
+
: undefined
|
|
160
|
+
const alternate = node.alternate
|
|
161
|
+
? compileAst(node.alternate, grammar)
|
|
162
|
+
: undefined
|
|
163
|
+
return (ev) => {
|
|
164
|
+
const res = test(ev)
|
|
165
|
+
if (res) {
|
|
166
|
+
// an omitted consequent ("a ?: b") yields the test result
|
|
167
|
+
return consequent ? consequent(ev) : res
|
|
168
|
+
}
|
|
169
|
+
return alternate ? alternate(ev) : undefined
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'FilterExpression': {
|
|
174
|
+
if (node.relative) {
|
|
175
|
+
// thrown on evaluation rather than compilation, as before
|
|
176
|
+
return () => {
|
|
177
|
+
throw new Error('Relative filter expressions are not supported')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const subject = compileAst(node.subject, grammar)
|
|
181
|
+
const index = compileAst(node.expr, grammar)
|
|
182
|
+
return (ev) => {
|
|
183
|
+
const subjectVal = subject(ev)
|
|
184
|
+
const indexVal = index(ev)
|
|
185
|
+
if (subjectVal == null) {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
return typeof indexVal === 'string' || typeof indexVal === 'number'
|
|
189
|
+
? (subjectVal as Record<string | number, JexlValue>)[indexVal]
|
|
190
|
+
: undefined
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'ArrayLiteral': {
|
|
195
|
+
const items = node.value.map((item) => compileAst(item, grammar))
|
|
196
|
+
const len = items.length
|
|
197
|
+
return (ev) => {
|
|
198
|
+
const out: JexlValue[] = new Array(len)
|
|
199
|
+
for (let i = 0; i < len; i++) {
|
|
200
|
+
out[i] = items[i]!(ev)
|
|
201
|
+
}
|
|
202
|
+
return out
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case 'ObjectLiteral': {
|
|
207
|
+
const entries = Object.entries(node.value)
|
|
208
|
+
const keys = entries.map(([key]) => key)
|
|
209
|
+
const values = entries.map(([, value]) => compileAst(value, grammar))
|
|
210
|
+
const len = keys.length
|
|
211
|
+
// resolved once, here, so the common case keeps its plain store
|
|
212
|
+
const store = keys.includes('__proto__') ? defineOwn : assignOwn
|
|
213
|
+
return (ev) => {
|
|
214
|
+
const out: Record<string, JexlValue> = {}
|
|
215
|
+
for (let i = 0; i < len; i++) {
|
|
216
|
+
store(out, keys[i]!, values[i]!(ev))
|
|
217
|
+
}
|
|
218
|
+
return out
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'TemplateLiteral': {
|
|
223
|
+
// every part renders to a string, so these are narrower than CompiledNode
|
|
224
|
+
const parts = node.parts.map((part): ((ev: Evaluator) => string) => {
|
|
225
|
+
if (part.type === 'static') {
|
|
226
|
+
const { value } = part
|
|
227
|
+
return () => value
|
|
228
|
+
}
|
|
229
|
+
const expr = compileAst(part.value, grammar)
|
|
230
|
+
return (ev) => stringify(expr(ev))
|
|
231
|
+
})
|
|
232
|
+
return (ev) => {
|
|
233
|
+
let out = ''
|
|
234
|
+
for (const part of parts) {
|
|
235
|
+
out += part(ev)
|
|
236
|
+
}
|
|
237
|
+
return out
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'FunctionCall': {
|
|
242
|
+
const { name } = node
|
|
243
|
+
const args = node.args.map((arg) => compileAst(arg, grammar))
|
|
244
|
+
// hasOwn, so that inherited Object.prototype members such as `toString`
|
|
245
|
+
// and `constructor` aren't callable as Jexl functions. Resolved per call
|
|
246
|
+
// rather than baked in, since functions are routinely registered after
|
|
247
|
+
// an expression has been compiled.
|
|
248
|
+
const lookup = (ev: Evaluator) => {
|
|
249
|
+
const { functions } = ev._grammar
|
|
250
|
+
if (!Object.hasOwn(functions, name)) {
|
|
251
|
+
throw new Error(`Jexl Function ${name} is not defined.`)
|
|
252
|
+
}
|
|
253
|
+
return functions[name]!
|
|
254
|
+
}
|
|
255
|
+
// the small arities are spelled out so an ordinary call allocates no
|
|
256
|
+
// argument array and needs no spread
|
|
257
|
+
switch (args.length) {
|
|
258
|
+
case 0: {
|
|
259
|
+
return (ev) => lookup(ev)()
|
|
260
|
+
}
|
|
261
|
+
case 1: {
|
|
262
|
+
const [a0] = args as [CompiledNode]
|
|
263
|
+
return (ev) => lookup(ev)(a0(ev))
|
|
264
|
+
}
|
|
265
|
+
case 2: {
|
|
266
|
+
const [a0, a1] = args as [CompiledNode, CompiledNode]
|
|
267
|
+
return (ev) => lookup(ev)(a0(ev), a1(ev))
|
|
268
|
+
}
|
|
269
|
+
case 3: {
|
|
270
|
+
const [a0, a1, a2] = args as [
|
|
271
|
+
CompiledNode,
|
|
272
|
+
CompiledNode,
|
|
273
|
+
CompiledNode
|
|
274
|
+
]
|
|
275
|
+
return (ev) => lookup(ev)(a0(ev), a1(ev), a2(ev))
|
|
276
|
+
}
|
|
277
|
+
default: {
|
|
278
|
+
const len = args.length
|
|
279
|
+
return (ev) => {
|
|
280
|
+
const vals: JexlValue[] = new Array(len)
|
|
281
|
+
for (let i = 0; i < len; i++) {
|
|
282
|
+
vals[i] = args[i]!(ev)
|
|
283
|
+
}
|
|
284
|
+
return lookup(ev)(...vals)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'SequenceExpression': {
|
|
291
|
+
const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
|
|
292
|
+
const len = exprs.length
|
|
293
|
+
return (ev) => {
|
|
294
|
+
let last: JexlValue
|
|
295
|
+
for (let i = 0; i < len; i++) {
|
|
296
|
+
last = exprs[i]!(ev)
|
|
297
|
+
}
|
|
298
|
+
return last
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'AssignmentExpression': {
|
|
303
|
+
const name = node.left.value
|
|
304
|
+
const right = compileAst(node.right!, grammar)
|
|
305
|
+
const store = name === '__proto__' ? defineOwn : assignOwn
|
|
306
|
+
return (ev) => {
|
|
307
|
+
const value = right(ev)
|
|
308
|
+
store(ev._context, name, value)
|
|
309
|
+
return value
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
default: {
|
|
314
|
+
throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
package/src/grammar.ts
CHANGED
|
@@ -5,28 +5,70 @@
|
|
|
5
5
|
|
|
6
6
|
/* eslint eqeqeq:0 */
|
|
7
7
|
|
|
8
|
+
import type { JexlValue } from './types.ts'
|
|
9
|
+
|
|
10
|
+
export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
|
|
11
|
+
|
|
12
|
+
export type BinaryOpEvalOnDemand = (
|
|
13
|
+
left: { eval: () => JexlValue },
|
|
14
|
+
right: { eval: () => JexlValue }
|
|
15
|
+
) => JexlValue
|
|
16
|
+
|
|
8
17
|
export interface BinaryOp {
|
|
9
18
|
type: 'binaryOp'
|
|
10
19
|
precedence: number
|
|
11
|
-
eval?:
|
|
12
|
-
evalOnDemand?:
|
|
20
|
+
eval?: BinaryOpEval
|
|
21
|
+
evalOnDemand?: BinaryOpEvalOnDemand
|
|
22
|
+
/**
|
|
23
|
+
* Evaluator for the operator's prefix form, if it has one. The Lexer emits a
|
|
24
|
+
* unaryOp token when such an operator appears where an operand is expected,
|
|
25
|
+
* which is how `-x` is distinguished from `a - x`.
|
|
26
|
+
*/
|
|
27
|
+
unaryEval?: (right: JexlValue) => JexlValue
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
export interface UnaryOp {
|
|
16
31
|
type: 'unaryOp'
|
|
17
32
|
precedence: number
|
|
18
|
-
eval: (right:
|
|
33
|
+
eval: (right: JexlValue) => JexlValue
|
|
19
34
|
}
|
|
20
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The punctuation elements of the grammar. Unlike operators these carry no
|
|
38
|
+
* behavior of their own; the Parser's state machine gives them meaning. The
|
|
39
|
+
* type is a literal union rather than `string` so that `GrammarElement` is a
|
|
40
|
+
* discriminated union, letting operator properties be accessed without casts.
|
|
41
|
+
*/
|
|
21
42
|
export interface SimpleElement {
|
|
22
|
-
type:
|
|
43
|
+
type:
|
|
44
|
+
| 'dot'
|
|
45
|
+
| 'openBracket'
|
|
46
|
+
| 'closeBracket'
|
|
47
|
+
| 'openCurl'
|
|
48
|
+
| 'closeCurl'
|
|
49
|
+
| 'colon'
|
|
50
|
+
| 'comma'
|
|
51
|
+
| 'openParen'
|
|
52
|
+
| 'closeParen'
|
|
53
|
+
| 'question'
|
|
54
|
+
| 'semicolon'
|
|
23
55
|
}
|
|
24
56
|
|
|
25
57
|
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
26
58
|
|
|
27
59
|
export interface Grammar {
|
|
28
60
|
elements: Record<string, GrammarElement>
|
|
29
|
-
functions: Record<string, (...args:
|
|
61
|
+
functions: Record<string, (...args: JexlValue[]) => JexlValue>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the binding power of a grammar element, or 0 for elements that
|
|
66
|
+
* aren't operators and therefore don't participate in precedence.
|
|
67
|
+
*/
|
|
68
|
+
export function precedenceOf(elem: GrammarElement | undefined) {
|
|
69
|
+
return elem && (elem.type === 'binaryOp' || elem.type === 'unaryOp')
|
|
70
|
+
? elem.precedence
|
|
71
|
+
: 0
|
|
30
72
|
}
|
|
31
73
|
|
|
32
74
|
export const getGrammar = (): Grammar => ({
|
|
@@ -50,37 +92,38 @@ export const getGrammar = (): Grammar => ({
|
|
|
50
92
|
'+': {
|
|
51
93
|
type: 'binaryOp',
|
|
52
94
|
precedence: 30,
|
|
53
|
-
eval: (left, right) => left + right
|
|
95
|
+
eval: (left, right) => (left as number) + (right as number)
|
|
54
96
|
},
|
|
55
97
|
'-': {
|
|
56
98
|
type: 'binaryOp',
|
|
57
99
|
precedence: 30,
|
|
58
|
-
eval: (left, right) => left - right
|
|
100
|
+
eval: (left, right) => (left as number) - (right as number),
|
|
101
|
+
unaryEval: (right) => -(right as number)
|
|
59
102
|
},
|
|
60
103
|
'*': {
|
|
61
104
|
type: 'binaryOp',
|
|
62
105
|
precedence: 40,
|
|
63
|
-
eval: (left, right) => left * right
|
|
106
|
+
eval: (left, right) => (left as number) * (right as number)
|
|
64
107
|
},
|
|
65
108
|
'/': {
|
|
66
109
|
type: 'binaryOp',
|
|
67
110
|
precedence: 40,
|
|
68
|
-
eval: (left, right) => left / right
|
|
111
|
+
eval: (left, right) => (left as number) / (right as number)
|
|
69
112
|
},
|
|
70
113
|
'//': {
|
|
71
114
|
type: 'binaryOp',
|
|
72
115
|
precedence: 40,
|
|
73
|
-
eval: (left, right) => Math.floor(left / right)
|
|
116
|
+
eval: (left, right) => Math.floor((left as number) / (right as number))
|
|
74
117
|
},
|
|
75
118
|
'%': {
|
|
76
119
|
type: 'binaryOp',
|
|
77
120
|
precedence: 50,
|
|
78
|
-
eval: (left, right) => left % right
|
|
121
|
+
eval: (left, right) => (left as number) % (right as number)
|
|
79
122
|
},
|
|
80
123
|
'^': {
|
|
81
124
|
type: 'binaryOp',
|
|
82
125
|
precedence: 50,
|
|
83
|
-
eval: (left, right) =>
|
|
126
|
+
eval: (left, right) => (left as number) ** (right as number)
|
|
84
127
|
},
|
|
85
128
|
'==': {
|
|
86
129
|
type: 'binaryOp',
|
|
@@ -95,22 +138,22 @@ export const getGrammar = (): Grammar => ({
|
|
|
95
138
|
'>': {
|
|
96
139
|
type: 'binaryOp',
|
|
97
140
|
precedence: 20,
|
|
98
|
-
eval: (left, right) => left > right
|
|
141
|
+
eval: (left, right) => (left as number) > (right as number)
|
|
99
142
|
},
|
|
100
143
|
'>=': {
|
|
101
144
|
type: 'binaryOp',
|
|
102
145
|
precedence: 20,
|
|
103
|
-
eval: (left, right) => left >= right
|
|
146
|
+
eval: (left, right) => (left as number) >= (right as number)
|
|
104
147
|
},
|
|
105
148
|
'<': {
|
|
106
149
|
type: 'binaryOp',
|
|
107
150
|
precedence: 20,
|
|
108
|
-
eval: (left, right) => left < right
|
|
151
|
+
eval: (left, right) => (left as number) < (right as number)
|
|
109
152
|
},
|
|
110
153
|
'<=': {
|
|
111
154
|
type: 'binaryOp',
|
|
112
155
|
precedence: 20,
|
|
113
|
-
eval: (left, right) => left <= right
|
|
156
|
+
eval: (left, right) => (left as number) <= (right as number)
|
|
114
157
|
},
|
|
115
158
|
'&&': {
|
|
116
159
|
type: 'binaryOp',
|
|
@@ -139,12 +182,17 @@ export const getGrammar = (): Grammar => ({
|
|
|
139
182
|
precedence: 20,
|
|
140
183
|
eval: (left, right) => {
|
|
141
184
|
if (typeof right === 'string') {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
// only a primitive has a meaningful substring form. An absent or
|
|
186
|
+
// structured left operand is not "in" a string, and must not be
|
|
187
|
+
// coerced to '' — every string contains the empty string, so that
|
|
188
|
+
// made `feature.missingAttr in "someString"` true for every feature.
|
|
189
|
+
return typeof left === 'string' ||
|
|
190
|
+
typeof left === 'number' ||
|
|
191
|
+
typeof left === 'boolean'
|
|
192
|
+
? right.includes(String(left))
|
|
193
|
+
: false
|
|
146
194
|
}
|
|
147
|
-
return false
|
|
195
|
+
return Array.isArray(right) ? right.includes(left) : false
|
|
148
196
|
}
|
|
149
197
|
},
|
|
150
198
|
'!': {
|
package/src/parser/Parser.ts
CHANGED
|
@@ -7,10 +7,26 @@ import * as handlers from './handlers.ts'
|
|
|
7
7
|
import { states } from './states.ts'
|
|
8
8
|
|
|
9
9
|
import type Lexer from '../Lexer.ts'
|
|
10
|
-
import type {
|
|
10
|
+
import type { Grammar } from '../grammar.ts'
|
|
11
|
+
import type { AstNode, SequenceExpression, Token } from '../types.ts'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The handler to run for a token type when the state it appears in doesn't
|
|
15
|
+
* name one of its own. Token types absent here — the brackets, parens and
|
|
16
|
+
* separators — carry no behavior beyond the state transition the state machine
|
|
17
|
+
* gives them.
|
|
18
|
+
*/
|
|
19
|
+
const tokenHandlers: Record<
|
|
20
|
+
string,
|
|
21
|
+
((this: Parser, token: Token) => void) | undefined
|
|
22
|
+
> = {
|
|
23
|
+
binaryOp: handlers.binaryOp,
|
|
24
|
+
dot: handlers.dot,
|
|
25
|
+
identifier: handlers.identifier,
|
|
26
|
+
literal: handlers.literal,
|
|
27
|
+
semicolon: handlers.semicolon,
|
|
28
|
+
templateString: handlers.templateString,
|
|
29
|
+
unaryOp: handlers.unaryOp
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
/**
|
|
@@ -37,7 +53,7 @@ class Parser {
|
|
|
37
53
|
_tree: AstNode | null
|
|
38
54
|
_exprStr: string
|
|
39
55
|
_relative: boolean
|
|
40
|
-
_stopMap: Record<string,
|
|
56
|
+
_stopMap: Record<string, string>
|
|
41
57
|
_cursor?: AstNode | null
|
|
42
58
|
_subParser?: Parser
|
|
43
59
|
_parentStop?: boolean
|
|
@@ -50,7 +66,7 @@ class Parser {
|
|
|
50
66
|
grammar: Grammar,
|
|
51
67
|
lexer: Lexer,
|
|
52
68
|
prefix?: string,
|
|
53
|
-
stopMap?: Record<string,
|
|
69
|
+
stopMap?: Record<string, string>
|
|
54
70
|
) {
|
|
55
71
|
this._grammar = grammar
|
|
56
72
|
this._lexer = lexer
|
|
@@ -71,18 +87,16 @@ class Parser {
|
|
|
71
87
|
* @returns {boolean|*} the stopState value if this parser encountered a token
|
|
72
88
|
* in the stopState mapb false if tokens can continue.
|
|
73
89
|
*/
|
|
74
|
-
addToken(token: Token):
|
|
90
|
+
addToken(token: Token): string | false {
|
|
75
91
|
if (this._state === 'complete') {
|
|
76
92
|
throw new Error('Cannot add a new token to a completed Parser')
|
|
77
93
|
}
|
|
78
|
-
const state = states[this._state]
|
|
94
|
+
const state = states[this._state]!
|
|
79
95
|
const startExpr = this._exprStr
|
|
80
96
|
this._exprStr += token.raw
|
|
81
97
|
if (state.subHandler) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
const stopState = this._subParser!.addToken(token)
|
|
98
|
+
const subParser = this._subParser ?? this._startSubExpression(startExpr)
|
|
99
|
+
const stopState = subParser.addToken(token)
|
|
86
100
|
if (stopState) {
|
|
87
101
|
this._endSubExpression()
|
|
88
102
|
if (stopState === '_semicolon') {
|
|
@@ -94,21 +108,18 @@ class Parser {
|
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
} else if (token.type === 'semicolon' && this._stopMap[token.type]) {
|
|
97
|
-
return this._stopMap[token.type]
|
|
111
|
+
return this._stopMap[token.type]!
|
|
98
112
|
} else if (state.tokenTypes?.[token.type]) {
|
|
99
|
-
const typeOpts = state.tokenTypes[token.type]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (handleFunc) {
|
|
105
|
-
handleFunc.call(this, token)
|
|
106
|
-
}
|
|
113
|
+
const typeOpts = state.tokenTypes[token.type]!
|
|
114
|
+
// the state's own handler wins over the token type's default one, and a
|
|
115
|
+
// token type with neither only drives the transition below
|
|
116
|
+
const handleFunc = typeOpts.handler ?? tokenHandlers[token.type]
|
|
117
|
+
handleFunc?.call(this, token)
|
|
107
118
|
if (typeOpts.toState) {
|
|
108
119
|
this._state = typeOpts.toState
|
|
109
120
|
}
|
|
110
121
|
} else if (this._stopMap[token.type]) {
|
|
111
|
-
return this._stopMap[token.type]
|
|
122
|
+
return this._stopMap[token.type]!
|
|
112
123
|
} else {
|
|
113
124
|
throw new Error(
|
|
114
125
|
`Token ${token.raw} (${token.type}) unexpected in expression: ${this._exprStr}`
|
|
@@ -124,7 +135,9 @@ class Parser {
|
|
|
124
135
|
* the {@link Lexer#tokenize} function.
|
|
125
136
|
*/
|
|
126
137
|
addTokens(tokens: Token[]) {
|
|
127
|
-
|
|
138
|
+
for (const token of tokens) {
|
|
139
|
+
this.addToken(token)
|
|
140
|
+
}
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
/**
|
|
@@ -136,7 +149,14 @@ class Parser {
|
|
|
136
149
|
* the expression, indicating that the expression is incomplete
|
|
137
150
|
*/
|
|
138
151
|
complete() {
|
|
139
|
-
|
|
152
|
+
// `_subParser` counts as unfinished business alongside `_cursor`: a group
|
|
153
|
+
// that opened before anything was placed in the tree, as in "(1", leaves
|
|
154
|
+
// the cursor null, and testing the cursor alone let the missing ")" pass
|
|
155
|
+
// silently and evaluate as if it were closed.
|
|
156
|
+
if (
|
|
157
|
+
(this._cursor || this._subParser) &&
|
|
158
|
+
!states[this._state]!.completable
|
|
159
|
+
) {
|
|
140
160
|
throw new Error(`Unexpected end of expression: ${this._exprStr}`)
|
|
141
161
|
}
|
|
142
162
|
if (this._subParser) {
|
|
@@ -147,7 +167,7 @@ class Parser {
|
|
|
147
167
|
if (this._tree) {
|
|
148
168
|
this._sequenceExpressions.push(this._tree)
|
|
149
169
|
}
|
|
150
|
-
const sequence:
|
|
170
|
+
const sequence: SequenceExpression = {
|
|
151
171
|
type: 'SequenceExpression',
|
|
152
172
|
expressions: this._sequenceExpressions
|
|
153
173
|
}
|
|
@@ -173,7 +193,7 @@ class Parser {
|
|
|
173
193
|
* @private
|
|
174
194
|
*/
|
|
175
195
|
_endSubExpression() {
|
|
176
|
-
states[this._state]
|
|
196
|
+
states[this._state]!.subHandler!.call(this, this._subParser!.complete())
|
|
177
197
|
this._subParser = undefined
|
|
178
198
|
}
|
|
179
199
|
|
|
@@ -188,7 +208,7 @@ class Parser {
|
|
|
188
208
|
if (!this._cursor) {
|
|
189
209
|
this._tree = node
|
|
190
210
|
} else {
|
|
191
|
-
|
|
211
|
+
this._cursor.right = node
|
|
192
212
|
this._setParent(node, this._cursor)
|
|
193
213
|
}
|
|
194
214
|
this._cursor = node
|
|
@@ -227,18 +247,20 @@ class Parser {
|
|
|
227
247
|
* Prepares the Parser to accept a subexpression by (re)instantiating the
|
|
228
248
|
* subParser.
|
|
229
249
|
* @param {string} [exprStr] The expression string to prefix to the new Parser
|
|
250
|
+
* @returns {Parser} the new subParser, also stored on this instance
|
|
230
251
|
* @private
|
|
231
252
|
*/
|
|
232
253
|
_startSubExpression(exprStr?: string) {
|
|
233
|
-
let endStates = states[this._state]
|
|
254
|
+
let endStates = states[this._state]!.endStates
|
|
234
255
|
if (!endStates) {
|
|
235
256
|
this._parentStop = true
|
|
236
257
|
endStates = this._stopMap
|
|
237
258
|
}
|
|
238
|
-
if (states[this._state]
|
|
259
|
+
if (states[this._state]!.completable && !endStates.semicolon) {
|
|
239
260
|
endStates = { ...endStates, semicolon: '_semicolon' }
|
|
240
261
|
}
|
|
241
262
|
this._subParser = new Parser(this._grammar, this._lexer, exprStr, endStates)
|
|
263
|
+
return this._subParser
|
|
242
264
|
}
|
|
243
265
|
}
|
|
244
266
|
|