@jbrowse/jexl 3.2.0 → 4.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/dist/Expression.js +1 -2
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +8 -8
- package/dist/Jexl.js +7 -1
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +10 -8
- package/dist/Lexer.js +46 -35
- package/dist/Lexer.js.map +1 -1
- package/dist/evaluator/compile.d.ts +6 -4
- package/dist/evaluator/compile.js +42 -52
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +19 -2
- package/dist/grammar.js +1 -1
- package/dist/grammar.js.map +1 -1
- package/dist/parser/Parser.d.ts +2 -9
- package/dist/parser/Parser.js +2 -12
- package/dist/parser/Parser.js.map +1 -1
- package/dist/parser/handlers.d.ts +6 -3
- package/dist/parser/handlers.js +45 -28
- package/dist/parser/handlers.js.map +1 -1
- package/dist/parser/states.js +3 -5
- package/dist/parser/states.js.map +1 -1
- package/dist/types.d.ts +0 -3
- package/esm/Expression.js +1 -2
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +8 -8
- package/esm/Jexl.js +7 -1
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +10 -8
- package/esm/Lexer.js +46 -35
- package/esm/Lexer.js.map +1 -1
- package/esm/evaluator/compile.d.ts +6 -4
- package/esm/evaluator/compile.js +42 -52
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +19 -2
- package/esm/grammar.js +1 -1
- package/esm/grammar.js.map +1 -1
- package/esm/parser/Parser.d.ts +2 -9
- package/esm/parser/Parser.js +2 -12
- package/esm/parser/Parser.js.map +1 -1
- package/esm/parser/handlers.d.ts +6 -3
- package/esm/parser/handlers.js +45 -28
- package/esm/parser/handlers.js.map +1 -1
- package/esm/parser/states.js +3 -5
- package/esm/parser/states.js.map +1 -1
- package/esm/types.d.ts +0 -3
- package/package.json +2 -1
- package/src/Expression.ts +1 -2
- package/src/Jexl.ts +20 -11
- package/src/Lexer.ts +47 -36
- package/src/evaluator/compile.ts +53 -68
- package/src/grammar.ts +23 -3
- package/src/parser/Parser.ts +2 -13
- package/src/parser/handlers.ts +48 -31
- package/src/parser/states.ts +3 -5
- package/src/types.ts +0 -3
- package/dist/evaluator/Evaluator.d.ts +0 -37
- package/dist/evaluator/Evaluator.js +0 -49
- package/dist/evaluator/Evaluator.js.map +0 -1
- package/esm/evaluator/Evaluator.d.ts +0 -37
- package/esm/evaluator/Evaluator.js +0 -47
- package/esm/evaluator/Evaluator.js.map +0 -1
- package/src/evaluator/Evaluator.ts +0 -58
package/src/evaluator/compile.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Grammar } from '../grammar.ts'
|
|
7
7
|
import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
/** The variables an expression is evaluated against. */
|
|
10
|
+
export type Context = Record<string, JexlValue>
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* An AST node lowered to a closure. Calling it with
|
|
12
|
-
*
|
|
13
|
+
* An AST node lowered to a closure. Calling it with a context produces the
|
|
14
|
+
* node's value. The grammar is captured when the closure is built, so it is not
|
|
15
|
+
* a parameter.
|
|
13
16
|
*/
|
|
14
|
-
export type CompiledNode = (
|
|
17
|
+
export type CompiledNode = (context: Context) => JexlValue
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Writes a key that a plain assignment would mishandle. Storing to
|
|
@@ -20,11 +23,7 @@ export type CompiledNode = (ev: Evaluator) => JexlValue
|
|
|
20
23
|
* re-pointed the target's prototype). Defining the property instead matches
|
|
21
24
|
* what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
|
|
22
25
|
*/
|
|
23
|
-
function defineOwn(
|
|
24
|
-
target: Record<string, JexlValue>,
|
|
25
|
-
key: string,
|
|
26
|
-
value: JexlValue
|
|
27
|
-
) {
|
|
26
|
+
function defineOwn(target: Context, key: string, value: JexlValue) {
|
|
28
27
|
Object.defineProperty(target, key, {
|
|
29
28
|
value,
|
|
30
29
|
writable: true,
|
|
@@ -33,11 +32,7 @@ function defineOwn(
|
|
|
33
32
|
})
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function assignOwn(
|
|
37
|
-
target: Record<string, JexlValue>,
|
|
38
|
-
key: string,
|
|
39
|
-
value: JexlValue
|
|
40
|
-
) {
|
|
35
|
+
function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
41
36
|
target[key] = value
|
|
42
37
|
}
|
|
43
38
|
|
|
@@ -88,21 +83,17 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
88
83
|
case 'Identifier': {
|
|
89
84
|
const name = node.value
|
|
90
85
|
if (!node.from) {
|
|
91
|
-
return
|
|
92
|
-
? (ev) => ev._relContext[name]
|
|
93
|
-
: (ev) => ev._context[name]
|
|
86
|
+
return (ctx) => ctx[name]
|
|
94
87
|
}
|
|
95
88
|
const from = compileAst(node.from, grammar)
|
|
96
|
-
return (
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
89
|
+
return (ctx) => {
|
|
90
|
+
const subject = from(ctx)
|
|
91
|
+
if (subject == null) {
|
|
99
92
|
return undefined
|
|
100
93
|
}
|
|
101
94
|
// an identifier chained off an array reads through its first element
|
|
102
|
-
const
|
|
103
|
-
return
|
|
104
|
-
? undefined
|
|
105
|
-
: (ctx as Record<string, JexlValue>)[name]
|
|
95
|
+
const target = Array.isArray(subject) ? subject[0] : subject
|
|
96
|
+
return target == null ? undefined : (target as Context)[name]
|
|
106
97
|
}
|
|
107
98
|
}
|
|
108
99
|
|
|
@@ -119,18 +110,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
119
110
|
if (evalOnDemand) {
|
|
120
111
|
// operands stay unevaluated behind an `eval` thunk, so operators such
|
|
121
112
|
// as && and || can short-circuit
|
|
122
|
-
return (
|
|
123
|
-
evalOnDemand({ eval: () => left(
|
|
113
|
+
return (ctx) =>
|
|
114
|
+
evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
|
|
124
115
|
}
|
|
125
116
|
const fn = op.eval
|
|
126
117
|
if (!fn) {
|
|
127
|
-
return (
|
|
128
|
-
left(
|
|
129
|
-
right(
|
|
118
|
+
return (ctx) => {
|
|
119
|
+
left(ctx)
|
|
120
|
+
right(ctx)
|
|
130
121
|
return undefined
|
|
131
122
|
}
|
|
132
123
|
}
|
|
133
|
-
return (
|
|
124
|
+
return (ctx) => fn(left(ctx), right(ctx))
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
case 'UnaryExpression': {
|
|
@@ -144,12 +135,12 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
144
135
|
: undefined
|
|
145
136
|
if (!fn) {
|
|
146
137
|
// the operand is still evaluated for an unknown operator, as before
|
|
147
|
-
return (
|
|
148
|
-
right(
|
|
138
|
+
return (ctx) => {
|
|
139
|
+
right(ctx)
|
|
149
140
|
return undefined
|
|
150
141
|
}
|
|
151
142
|
}
|
|
152
|
-
return (
|
|
143
|
+
return (ctx) => fn(right(ctx))
|
|
153
144
|
}
|
|
154
145
|
|
|
155
146
|
case 'ConditionalExpression': {
|
|
@@ -160,28 +151,22 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
160
151
|
const alternate = node.alternate
|
|
161
152
|
? compileAst(node.alternate, grammar)
|
|
162
153
|
: undefined
|
|
163
|
-
return (
|
|
164
|
-
const res = test(
|
|
154
|
+
return (ctx) => {
|
|
155
|
+
const res = test(ctx)
|
|
165
156
|
if (res) {
|
|
166
157
|
// an omitted consequent ("a ?: b") yields the test result
|
|
167
|
-
return consequent ? consequent(
|
|
158
|
+
return consequent ? consequent(ctx) : res
|
|
168
159
|
}
|
|
169
|
-
return alternate ? alternate(
|
|
160
|
+
return alternate ? alternate(ctx) : undefined
|
|
170
161
|
}
|
|
171
162
|
}
|
|
172
163
|
|
|
173
164
|
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
165
|
const subject = compileAst(node.subject, grammar)
|
|
181
166
|
const index = compileAst(node.expr, grammar)
|
|
182
|
-
return (
|
|
183
|
-
const subjectVal = subject(
|
|
184
|
-
const indexVal = index(
|
|
167
|
+
return (ctx) => {
|
|
168
|
+
const subjectVal = subject(ctx)
|
|
169
|
+
const indexVal = index(ctx)
|
|
185
170
|
if (subjectVal == null) {
|
|
186
171
|
return undefined
|
|
187
172
|
}
|
|
@@ -194,10 +179,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
194
179
|
case 'ArrayLiteral': {
|
|
195
180
|
const items = node.value.map((item) => compileAst(item, grammar))
|
|
196
181
|
const len = items.length
|
|
197
|
-
return (
|
|
182
|
+
return (ctx) => {
|
|
198
183
|
const out: JexlValue[] = new Array(len)
|
|
199
184
|
for (let i = 0; i < len; i++) {
|
|
200
|
-
out[i] = items[i]!(
|
|
185
|
+
out[i] = items[i]!(ctx)
|
|
201
186
|
}
|
|
202
187
|
return out
|
|
203
188
|
}
|
|
@@ -210,10 +195,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
210
195
|
const len = keys.length
|
|
211
196
|
// resolved once, here, so the common case keeps its plain store
|
|
212
197
|
const store = keys.includes('__proto__') ? defineOwn : assignOwn
|
|
213
|
-
return (
|
|
198
|
+
return (ctx) => {
|
|
214
199
|
const out: Record<string, JexlValue> = {}
|
|
215
200
|
for (let i = 0; i < len; i++) {
|
|
216
|
-
store(out, keys[i]!, values[i]!(
|
|
201
|
+
store(out, keys[i]!, values[i]!(ctx))
|
|
217
202
|
}
|
|
218
203
|
return out
|
|
219
204
|
}
|
|
@@ -221,18 +206,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
221
206
|
|
|
222
207
|
case 'TemplateLiteral': {
|
|
223
208
|
// every part renders to a string, so these are narrower than CompiledNode
|
|
224
|
-
const parts = node.parts.map((part): ((
|
|
209
|
+
const parts = node.parts.map((part): ((ctx: Context) => string) => {
|
|
225
210
|
if (part.type === 'static') {
|
|
226
211
|
const { value } = part
|
|
227
212
|
return () => value
|
|
228
213
|
}
|
|
229
214
|
const expr = compileAst(part.value, grammar)
|
|
230
|
-
return (
|
|
215
|
+
return (ctx) => stringify(expr(ctx))
|
|
231
216
|
})
|
|
232
|
-
return (
|
|
217
|
+
return (ctx) => {
|
|
233
218
|
let out = ''
|
|
234
219
|
for (const part of parts) {
|
|
235
|
-
out += part(
|
|
220
|
+
out += part(ctx)
|
|
236
221
|
}
|
|
237
222
|
return out
|
|
238
223
|
}
|
|
@@ -245,8 +230,8 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
245
230
|
// and `constructor` aren't callable as Jexl functions. Resolved per call
|
|
246
231
|
// rather than baked in, since functions are routinely registered after
|
|
247
232
|
// an expression has been compiled.
|
|
248
|
-
const lookup = (
|
|
249
|
-
const { functions } =
|
|
233
|
+
const lookup = () => {
|
|
234
|
+
const { functions } = grammar
|
|
250
235
|
if (!Object.hasOwn(functions, name)) {
|
|
251
236
|
throw new Error(`Jexl Function ${name} is not defined.`)
|
|
252
237
|
}
|
|
@@ -256,15 +241,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
256
241
|
// argument array and needs no spread
|
|
257
242
|
switch (args.length) {
|
|
258
243
|
case 0: {
|
|
259
|
-
return (
|
|
244
|
+
return () => lookup()()
|
|
260
245
|
}
|
|
261
246
|
case 1: {
|
|
262
247
|
const [a0] = args as [CompiledNode]
|
|
263
|
-
return (
|
|
248
|
+
return (ctx) => lookup()(a0(ctx))
|
|
264
249
|
}
|
|
265
250
|
case 2: {
|
|
266
251
|
const [a0, a1] = args as [CompiledNode, CompiledNode]
|
|
267
|
-
return (
|
|
252
|
+
return (ctx) => lookup()(a0(ctx), a1(ctx))
|
|
268
253
|
}
|
|
269
254
|
case 3: {
|
|
270
255
|
const [a0, a1, a2] = args as [
|
|
@@ -272,16 +257,16 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
272
257
|
CompiledNode,
|
|
273
258
|
CompiledNode
|
|
274
259
|
]
|
|
275
|
-
return (
|
|
260
|
+
return (ctx) => lookup()(a0(ctx), a1(ctx), a2(ctx))
|
|
276
261
|
}
|
|
277
262
|
default: {
|
|
278
263
|
const len = args.length
|
|
279
|
-
return (
|
|
264
|
+
return (ctx) => {
|
|
280
265
|
const vals: JexlValue[] = new Array(len)
|
|
281
266
|
for (let i = 0; i < len; i++) {
|
|
282
|
-
vals[i] = args[i]!(
|
|
267
|
+
vals[i] = args[i]!(ctx)
|
|
283
268
|
}
|
|
284
|
-
return lookup(
|
|
269
|
+
return lookup()(...vals)
|
|
285
270
|
}
|
|
286
271
|
}
|
|
287
272
|
}
|
|
@@ -290,10 +275,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
290
275
|
case 'SequenceExpression': {
|
|
291
276
|
const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
|
|
292
277
|
const len = exprs.length
|
|
293
|
-
return (
|
|
278
|
+
return (ctx) => {
|
|
294
279
|
let last: JexlValue
|
|
295
280
|
for (let i = 0; i < len; i++) {
|
|
296
|
-
last = exprs[i]!(
|
|
281
|
+
last = exprs[i]!(ctx)
|
|
297
282
|
}
|
|
298
283
|
return last
|
|
299
284
|
}
|
|
@@ -303,9 +288,9 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
303
288
|
const name = node.left.value
|
|
304
289
|
const right = compileAst(node.right!, grammar)
|
|
305
290
|
const store = name === '__proto__' ? defineOwn : assignOwn
|
|
306
|
-
return (
|
|
307
|
-
const value = right(
|
|
308
|
-
store(
|
|
291
|
+
return (ctx) => {
|
|
292
|
+
const value = right(ctx)
|
|
293
|
+
store(ctx, name, value)
|
|
309
294
|
return value
|
|
310
295
|
}
|
|
311
296
|
}
|
package/src/grammar.ts
CHANGED
|
@@ -9,6 +9,21 @@ import type { JexlValue } from './types.ts'
|
|
|
9
9
|
|
|
10
10
|
export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A function or operator implementation as its author writes it, for the
|
|
14
|
+
* registration methods on {@link Jexl} to accept.
|
|
15
|
+
*
|
|
16
|
+
* Jexl cannot check what a registered callback will be handed. The arguments
|
|
17
|
+
* are whatever the expression evaluated to, drawn from a context supplied at
|
|
18
|
+
* evaluation time, and that context routinely holds values jexl has no literal
|
|
19
|
+
* for — a host object with methods, a callback, a class instance. Requiring
|
|
20
|
+
* {@link JexlValue} parameters therefore rejected every callback actually
|
|
21
|
+
* written against such a value, since parameters are contravariant. The
|
|
22
|
+
* argument types are the caller's business; jexl only promises to pass along
|
|
23
|
+
* whatever the operands evaluated to.
|
|
24
|
+
*/
|
|
25
|
+
export type UncheckedFn = (...args: never[]) => unknown
|
|
26
|
+
|
|
12
27
|
export type BinaryOpEvalOnDemand = (
|
|
13
28
|
left: { eval: () => JexlValue },
|
|
14
29
|
right: { eval: () => JexlValue }
|
|
@@ -27,10 +42,12 @@ export interface BinaryOp {
|
|
|
27
42
|
unaryEval?: (right: JexlValue) => JexlValue
|
|
28
43
|
}
|
|
29
44
|
|
|
45
|
+
export type UnaryOpEval = (right: JexlValue) => JexlValue
|
|
46
|
+
|
|
30
47
|
export interface UnaryOp {
|
|
31
48
|
type: 'unaryOp'
|
|
32
49
|
precedence: number
|
|
33
|
-
eval:
|
|
50
|
+
eval: UnaryOpEval
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
/**
|
|
@@ -56,9 +73,12 @@ export interface SimpleElement {
|
|
|
56
73
|
|
|
57
74
|
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
58
75
|
|
|
76
|
+
/** A registered function, as jexl calls it once the operands are evaluated. */
|
|
77
|
+
export type GrammarFn = (...args: JexlValue[]) => JexlValue
|
|
78
|
+
|
|
59
79
|
export interface Grammar {
|
|
60
80
|
elements: Record<string, GrammarElement>
|
|
61
|
-
functions: Record<string,
|
|
81
|
+
functions: Record<string, GrammarFn>
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
/**
|
|
@@ -211,7 +231,7 @@ export const getGrammar = (): Grammar => ({
|
|
|
211
231
|
|
|
212
232
|
/**
|
|
213
233
|
* A map of function names to javascript functions. A Jexl function
|
|
214
|
-
* takes zero
|
|
234
|
+
* takes zero or more arguments:
|
|
215
235
|
*
|
|
216
236
|
* - {*} ...args: A variable number of arguments passed to this function.
|
|
217
237
|
* All of these are pre-evaluated to their actual values before calling
|
package/src/parser/Parser.ts
CHANGED
|
@@ -32,7 +32,7 @@ const tokenHandlers: Record<
|
|
|
32
32
|
/**
|
|
33
33
|
* The Parser is a state machine that converts tokens from the {@link Lexer}
|
|
34
34
|
* into an Abstract Syntax Tree (AST), capable of being evaluated in any
|
|
35
|
-
* context by
|
|
35
|
+
* context by {@link compileAst}. The Parser expects that all tokens
|
|
36
36
|
* provided to it are legal and typed properly according to the grammar, but
|
|
37
37
|
* accepts that the tokens may still be in an invalid order or in some other
|
|
38
38
|
* unparsable configuration that requires it to throw an Error.
|
|
@@ -52,13 +52,11 @@ class Parser {
|
|
|
52
52
|
_state: string
|
|
53
53
|
_tree: AstNode | null
|
|
54
54
|
_exprStr: string
|
|
55
|
-
_relative: boolean
|
|
56
55
|
_stopMap: Record<string, string>
|
|
57
56
|
_cursor?: AstNode | null
|
|
58
57
|
_subParser?: Parser
|
|
59
58
|
_parentStop?: boolean
|
|
60
59
|
_nextIdentEncapsulate?: boolean
|
|
61
|
-
_nextIdentRelative?: boolean
|
|
62
60
|
_curObjKey?: string
|
|
63
61
|
_sequenceExpressions?: AstNode[]
|
|
64
62
|
|
|
@@ -73,7 +71,6 @@ class Parser {
|
|
|
73
71
|
this._state = 'expectOperand'
|
|
74
72
|
this._tree = null
|
|
75
73
|
this._exprStr = prefix || ''
|
|
76
|
-
this._relative = false
|
|
77
74
|
this._stopMap = stopMap || {}
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -143,7 +140,7 @@ class Parser {
|
|
|
143
140
|
/**
|
|
144
141
|
* Marks this Parser instance as completed and retrieves the full AST.
|
|
145
142
|
* @returns {{}|null} a full expression tree, ready for evaluation by the
|
|
146
|
-
* {@link
|
|
143
|
+
* {@link compileAst} function, or null if no tokens were passed to
|
|
147
144
|
* the parser before complete was called
|
|
148
145
|
* @throws {Error} if the parser is not in a state where it's legal to end
|
|
149
146
|
* the expression, indicating that the expression is incomplete
|
|
@@ -179,14 +176,6 @@ class Parser {
|
|
|
179
176
|
return this._cursor ? this._tree : null
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
/**
|
|
183
|
-
* Indicates whether the expression tree contains a relative path identifier.
|
|
184
|
-
* @returns {boolean} true if a relative identifier exists false otherwise.
|
|
185
|
-
*/
|
|
186
|
-
isRelative() {
|
|
187
|
-
return this._relative
|
|
188
|
-
}
|
|
189
|
-
|
|
190
179
|
/**
|
|
191
180
|
* Ends a subexpression by completing the subParser and passing its result
|
|
192
181
|
* to the subHandler configured in the current state.
|
package/src/parser/handlers.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function arrayVal(this: Parser, ast: AstNode | null) {
|
|
|
64
64
|
export function binaryOp(this: Parser, token: Token) {
|
|
65
65
|
if (token.value === '=') {
|
|
66
66
|
// `from` marks a member access such as `a.b`; only a bare name can be
|
|
67
|
-
// assigned to, since the
|
|
67
|
+
// assigned to, since the compiled closure writes straight into the context
|
|
68
68
|
if (
|
|
69
69
|
this._cursor?.type !== 'Identifier' ||
|
|
70
70
|
(this._cursor as Identifier).from
|
|
@@ -90,9 +90,15 @@ export function binaryOp(this: Parser, token: Token) {
|
|
|
90
90
|
if (!parentExpr.operator) {
|
|
91
91
|
break
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// a prefix operator binds tighter than any binary one, so `-x ^ 2` groups
|
|
94
|
+
// as `(-x) ^ 2` — the same way `-2 ^ 2` already does, since the Lexer folds
|
|
95
|
+
// that sign into the literal. Looking `-` up in the grammar finds its
|
|
96
|
+
// binary precedence instead, which is lower than `^`'s and gave `-(x ^ 2)`
|
|
97
|
+
const parentPrecedence =
|
|
98
|
+
parent.type === 'UnaryExpression'
|
|
99
|
+
? Infinity
|
|
100
|
+
: precedenceOf(this._grammar.elements[parentExpr.operator])
|
|
101
|
+
if (parentPrecedence < precedence) {
|
|
96
102
|
break
|
|
97
103
|
}
|
|
98
104
|
this._cursor = parent
|
|
@@ -115,18 +121,20 @@ export function binaryOp(this: Parser, token: Token) {
|
|
|
115
121
|
*/
|
|
116
122
|
export function dot(this: Parser) {
|
|
117
123
|
const cursor = this._cursor
|
|
118
|
-
//
|
|
119
|
-
// still waiting on its right-hand side
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// a dot continues an identifier chain. With nothing to continue — no cursor,
|
|
125
|
+
// or an operator still waiting on its right-hand side — it would instead open
|
|
126
|
+
// a relative path, the leading dot of `list[.price > 5]`. This fork removed
|
|
127
|
+
// array filtering expressions, which are the only construct that gives a
|
|
128
|
+
// relative path a root to resolve against, so it is rejected here rather than
|
|
129
|
+
// parsed into a tree that can only fail once it is evaluated.
|
|
130
|
+
if (
|
|
131
|
+
!cursor ||
|
|
132
|
+
cursor.type === 'UnaryExpression' ||
|
|
133
|
+
(cursor.type === 'BinaryExpression' && !cursor.right)
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(`Relative paths are not supported: ${this._exprStr}`)
|
|
129
136
|
}
|
|
137
|
+
this._nextIdentEncapsulate = true
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
|
@@ -138,7 +146,6 @@ export function filter(this: Parser, ast: AstNode | null) {
|
|
|
138
146
|
const node: FilterExpression = {
|
|
139
147
|
type: 'FilterExpression',
|
|
140
148
|
expr: ast!,
|
|
141
|
-
relative: this._subParser!.isRelative(),
|
|
142
149
|
subject: this._cursor!
|
|
143
150
|
}
|
|
144
151
|
this._placeBeforeCursor(node)
|
|
@@ -162,12 +169,11 @@ export function functionCall(this: Parser) {
|
|
|
162
169
|
// there is nothing to look up in the function pool
|
|
163
170
|
throw new Error(`Functions must be called by name: ${this._exprStr}`)
|
|
164
171
|
}
|
|
165
|
-
const { from } = cursor as Identifier
|
|
172
|
+
const { from, value } = cursor as Identifier
|
|
166
173
|
const node: FunctionCall = {
|
|
167
174
|
type: 'FunctionCall',
|
|
168
|
-
name:
|
|
169
|
-
args: from ? [from] : []
|
|
170
|
-
pool: 'functions'
|
|
175
|
+
name: value,
|
|
176
|
+
args: from ? [from] : []
|
|
171
177
|
}
|
|
172
178
|
this._placeBeforeCursor(node)
|
|
173
179
|
}
|
|
@@ -186,10 +192,6 @@ export function identifier(this: Parser, token: Token) {
|
|
|
186
192
|
this._placeBeforeCursor(node)
|
|
187
193
|
this._nextIdentEncapsulate = false
|
|
188
194
|
} else {
|
|
189
|
-
if (this._nextIdentRelative) {
|
|
190
|
-
node.relative = true
|
|
191
|
-
this._nextIdentRelative = false
|
|
192
|
-
}
|
|
193
195
|
this._placeAtCursor(node)
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -274,7 +276,7 @@ export function objStart(this: Parser) {
|
|
|
274
276
|
export function objVal(this: Parser, ast: AstNode | null) {
|
|
275
277
|
// defined rather than assigned so that the key "__proto__" becomes an
|
|
276
278
|
// ordinary entry of the node's key map instead of re-pointing its prototype,
|
|
277
|
-
// which dropped the entry before
|
|
279
|
+
// which dropped the entry before it could be compiled
|
|
278
280
|
Object.defineProperty(
|
|
279
281
|
(this._cursor as ObjectLiteral).value,
|
|
280
282
|
this._curObjKey!,
|
|
@@ -313,17 +315,32 @@ export function ternaryMid(this: Parser, ast: AstNode | null) {
|
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
/**
|
|
316
|
-
* Handles the start of a new ternary expression by encapsulating the
|
|
317
|
-
*
|
|
318
|
-
*
|
|
318
|
+
* Handles the start of a new ternary expression by encapsulating the tree so
|
|
319
|
+
* far in a ConditionalExpression node, and using it as the test element.
|
|
320
|
+
*
|
|
321
|
+
* Every operator binds tighter than the conditional and so belongs in the test
|
|
322
|
+
* — except `=`, which binds looser. `x = a ? b : c` therefore assigns the
|
|
323
|
+
* conditional rather than testing the assignment, which had stored `a` in `x`.
|
|
319
324
|
*/
|
|
320
325
|
export function ternaryStart(this: Parser) {
|
|
326
|
+
let assignment: AstNode | undefined
|
|
327
|
+
let test = this._tree!
|
|
328
|
+
while (test.type === 'AssignmentExpression' && test.right) {
|
|
329
|
+
assignment = test
|
|
330
|
+
test = test.right
|
|
331
|
+
}
|
|
332
|
+
|
|
321
333
|
const node: ConditionalExpression = {
|
|
322
334
|
type: 'ConditionalExpression',
|
|
323
|
-
test
|
|
335
|
+
test
|
|
336
|
+
}
|
|
337
|
+
if (assignment) {
|
|
338
|
+
assignment.right = node
|
|
339
|
+
this._setParent(node, assignment)
|
|
340
|
+
} else {
|
|
341
|
+
this._tree = node
|
|
324
342
|
}
|
|
325
|
-
this.
|
|
326
|
-
this._cursor = this._tree
|
|
343
|
+
this._cursor = node
|
|
327
344
|
}
|
|
328
345
|
|
|
329
346
|
/**
|
package/src/parser/states.ts
CHANGED
|
@@ -97,10 +97,6 @@ export const states: States = {
|
|
|
97
97
|
colon: { toState: 'objVal' }
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
|
-
postArgs: {
|
|
101
|
-
tokenTypes: afterOperand,
|
|
102
|
-
completable: true
|
|
103
|
-
},
|
|
104
100
|
identifier: {
|
|
105
101
|
tokenTypes: {
|
|
106
102
|
...afterOperand,
|
|
@@ -131,7 +127,9 @@ export const states: States = {
|
|
|
131
127
|
subHandler: h.argVal,
|
|
132
128
|
endStates: {
|
|
133
129
|
comma: 'argVal',
|
|
134
|
-
|
|
130
|
+
// a call's result is an ordinary operand: it can be operated on or
|
|
131
|
+
// drilled into, but not called again, since only a name can be called
|
|
132
|
+
closeParen: 'expectBinOp'
|
|
135
133
|
}
|
|
136
134
|
},
|
|
137
135
|
objVal: {
|
package/src/types.ts
CHANGED
|
@@ -38,7 +38,6 @@ export interface Identifier extends AstNode {
|
|
|
38
38
|
type: 'Identifier'
|
|
39
39
|
value: string
|
|
40
40
|
from?: AstNode
|
|
41
|
-
relative?: boolean
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
export interface BinaryExpression extends AstNode {
|
|
@@ -77,13 +76,11 @@ export interface FunctionCall extends AstNode {
|
|
|
77
76
|
type: 'FunctionCall'
|
|
78
77
|
name: string
|
|
79
78
|
args: AstNode[]
|
|
80
|
-
pool: 'functions'
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
export interface FilterExpression extends AstNode {
|
|
84
82
|
type: 'FilterExpression'
|
|
85
83
|
expr: AstNode
|
|
86
|
-
relative: boolean
|
|
87
84
|
subject: AstNode
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { Grammar } from '../grammar.ts';
|
|
2
|
-
import type { AstNode, JexlValue } from '../types.ts';
|
|
3
|
-
/**
|
|
4
|
-
* The Evaluator carries everything an expression is evaluated against: the
|
|
5
|
-
* grammar, the context, and a relative context to be used as the root for
|
|
6
|
-
* relative identifiers. When any of these change, a new instance is required.
|
|
7
|
-
* However, a single instance can be used to evaluate many different
|
|
8
|
-
* expressions, and does not have to be reinstantiated for each.
|
|
9
|
-
*
|
|
10
|
-
* The work of evaluation itself lives in {@link compileAst}, which lowers a
|
|
11
|
-
* tree to closures. Prefer compiling once and reusing the result — that is what
|
|
12
|
-
* {@link Expression} does — over calling {@link #eval}, which compiles the tree
|
|
13
|
-
* it is given every time.
|
|
14
|
-
*
|
|
15
|
-
* @param {{}} grammar A grammar object against which to evaluate the expression
|
|
16
|
-
* tree
|
|
17
|
-
* @param {{}} [context] A map of variable keys to their values. This will be
|
|
18
|
-
* accessed to resolve the value of each non-relative identifier.
|
|
19
|
-
* @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
|
|
20
|
-
* to resolve the value of a relative identifier.
|
|
21
|
-
*/
|
|
22
|
-
declare class Evaluator {
|
|
23
|
-
_grammar: Grammar;
|
|
24
|
-
_context: Record<string, JexlValue>;
|
|
25
|
-
_relContext: Record<string, JexlValue>;
|
|
26
|
-
constructor(grammar: Grammar, context?: Record<string, JexlValue>, relativeContext?: Record<string, JexlValue>);
|
|
27
|
-
/**
|
|
28
|
-
* Compiles and evaluates an expression tree within the configured context.
|
|
29
|
-
* Convenient for a one-shot evaluation; for repeated evaluation of the same
|
|
30
|
-
* tree, hold onto the closure from {@link compileAst} instead so the tree is
|
|
31
|
-
* only lowered once.
|
|
32
|
-
* @param {{}} ast An expression tree object
|
|
33
|
-
* @returns {*} the resulting value of the expression.
|
|
34
|
-
*/
|
|
35
|
-
eval(ast: AstNode): JexlValue;
|
|
36
|
-
}
|
|
37
|
-
export default Evaluator;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* Jexl
|
|
4
|
-
* Copyright 2020 Tom Shawver
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const compile_ts_1 = require("./compile.js");
|
|
8
|
-
/**
|
|
9
|
-
* The Evaluator carries everything an expression is evaluated against: the
|
|
10
|
-
* grammar, the context, and a relative context to be used as the root for
|
|
11
|
-
* relative identifiers. When any of these change, a new instance is required.
|
|
12
|
-
* However, a single instance can be used to evaluate many different
|
|
13
|
-
* expressions, and does not have to be reinstantiated for each.
|
|
14
|
-
*
|
|
15
|
-
* The work of evaluation itself lives in {@link compileAst}, which lowers a
|
|
16
|
-
* tree to closures. Prefer compiling once and reusing the result — that is what
|
|
17
|
-
* {@link Expression} does — over calling {@link #eval}, which compiles the tree
|
|
18
|
-
* it is given every time.
|
|
19
|
-
*
|
|
20
|
-
* @param {{}} grammar A grammar object against which to evaluate the expression
|
|
21
|
-
* tree
|
|
22
|
-
* @param {{}} [context] A map of variable keys to their values. This will be
|
|
23
|
-
* accessed to resolve the value of each non-relative identifier.
|
|
24
|
-
* @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
|
|
25
|
-
* to resolve the value of a relative identifier.
|
|
26
|
-
*/
|
|
27
|
-
class Evaluator {
|
|
28
|
-
_grammar;
|
|
29
|
-
_context;
|
|
30
|
-
_relContext;
|
|
31
|
-
constructor(grammar, context, relativeContext) {
|
|
32
|
-
this._grammar = grammar;
|
|
33
|
-
this._context = context || {};
|
|
34
|
-
this._relContext = relativeContext || this._context;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Compiles and evaluates an expression tree within the configured context.
|
|
38
|
-
* Convenient for a one-shot evaluation; for repeated evaluation of the same
|
|
39
|
-
* tree, hold onto the closure from {@link compileAst} instead so the tree is
|
|
40
|
-
* only lowered once.
|
|
41
|
-
* @param {{}} ast An expression tree object
|
|
42
|
-
* @returns {*} the resulting value of the expression.
|
|
43
|
-
*/
|
|
44
|
-
eval(ast) {
|
|
45
|
-
return (0, compile_ts_1.compileAst)(ast, this._grammar)(this);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
exports.default = Evaluator;
|
|
49
|
-
//# sourceMappingURL=Evaluator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Evaluator.js","sourceRoot":"","sources":["../../src/evaluator/Evaluator.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAEH,6CAAyC;AAKzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,SAAS;IACb,QAAQ,CAAS;IACjB,QAAQ,CAA2B;IACnC,WAAW,CAA2B;IAEtC,YACE,OAAgB,EAChB,OAAmC,EACnC,eAA2C;QAE3C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;QACvB,IAAI,CAAC,QAAQ,GAAG,OAAO,IAAI,EAAE,CAAA;QAC7B,IAAI,CAAC,WAAW,GAAG,eAAe,IAAI,IAAI,CAAC,QAAQ,CAAA;IACrD,CAAC;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,GAAY;QACf,OAAO,IAAA,uBAAU,EAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC;CACF;AAED,kBAAe,SAAS,CAAA"}
|