@jbrowse/jexl 5.0.1 → 6.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/README.md +8 -3
- package/dist/Expression.d.ts +0 -12
- package/dist/Expression.js +0 -16
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.js +12 -16
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +12 -94
- package/dist/Lexer.js +41 -142
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.js +7 -7
- package/dist/analyze.js.map +1 -1
- package/dist/check.d.ts +0 -5
- package/dist/check.js +62 -240
- package/dist/check.js.map +1 -1
- package/dist/collections.d.ts +7 -0
- package/dist/collections.js +9 -2
- package/dist/collections.js.map +1 -1
- package/dist/conditions.d.ts +1 -1
- package/dist/conditions.js +28 -50
- package/dist/conditions.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/evaluator/compile.js +37 -24
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +1 -8
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +5 -0
- package/dist/operators.js +9 -5
- package/dist/operators.js.map +1 -1
- package/dist/parser/Parser.d.ts +11 -12
- package/dist/parser/Parser.js +4 -10
- package/dist/parser/Parser.js.map +1 -1
- package/dist/print.d.ts +9 -0
- package/dist/print.js +183 -0
- package/dist/print.js.map +1 -0
- package/dist/types.d.ts +18 -21
- package/esm/Expression.d.ts +0 -12
- package/esm/Expression.js +0 -16
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.js +12 -16
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +12 -94
- package/esm/Lexer.js +40 -142
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +2 -0
- package/esm/analyze.js +6 -7
- package/esm/analyze.js.map +1 -1
- package/esm/check.d.ts +0 -5
- package/esm/check.js +29 -206
- package/esm/check.js.map +1 -1
- package/esm/collections.d.ts +7 -0
- package/esm/collections.js +8 -1
- package/esm/collections.js.map +1 -1
- package/esm/conditions.d.ts +1 -1
- package/esm/conditions.js +27 -49
- package/esm/conditions.js.map +1 -1
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +7 -0
- package/esm/errors.js.map +1 -1
- package/esm/evaluator/compile.js +37 -24
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +2 -2
- package/esm/grammar.js +1 -8
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +2 -1
- package/esm/index.js +2 -1
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +5 -0
- package/esm/operators.js +8 -5
- package/esm/operators.js.map +1 -1
- package/esm/parser/Parser.d.ts +11 -12
- package/esm/parser/Parser.js +4 -8
- package/esm/parser/Parser.js.map +1 -1
- package/esm/print.d.ts +9 -0
- package/esm/print.js +177 -0
- package/esm/print.js.map +1 -0
- package/esm/types.d.ts +18 -21
- package/package.json +2 -1
- package/src/Expression.ts +0 -20
- package/src/Jexl.ts +13 -20
- package/src/Lexer.ts +42 -144
- package/src/analyze.ts +10 -10
- package/src/check.ts +48 -250
- package/src/collections.ts +11 -1
- package/src/conditions.ts +39 -62
- package/src/errors.ts +10 -0
- package/src/evaluator/compile.ts +49 -36
- package/src/grammar.ts +3 -9
- package/src/index.ts +2 -1
- package/src/operators.ts +10 -6
- package/src/parser/Parser.ts +13 -19
- package/src/print.ts +203 -0
- package/src/types.ts +19 -23
package/src/evaluator/compile.ts
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
* Copyright 2020 Tom Shawver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { LAMBDA } from '../collections.ts'
|
|
7
|
+
import { unknownNode } from '../errors.ts'
|
|
6
8
|
import { and, nullish, or } from '../grammar.ts'
|
|
7
9
|
|
|
8
10
|
import type { Grammar } from '../grammar.ts'
|
|
9
|
-
import type {
|
|
10
|
-
AstNode,
|
|
11
|
-
AstNodeUnion,
|
|
12
|
-
JexlFunction,
|
|
13
|
-
JexlValue,
|
|
14
|
-
Literal
|
|
15
|
-
} from '../types.ts'
|
|
11
|
+
import type { AstNode, JexlFunction, JexlValue } from '../types.ts'
|
|
16
12
|
|
|
17
13
|
/** The variables an expression is evaluated against. */
|
|
18
14
|
export type Context = Record<string, JexlValue>
|
|
@@ -44,6 +40,16 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
|
44
40
|
target[key] = value
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
// inherited, these lead from any value to `Function` and `Object.prototype`
|
|
44
|
+
const isUnsafeKey = (key: string | number) =>
|
|
45
|
+
key === 'constructor' || key === '__proto__'
|
|
46
|
+
|
|
47
|
+
function member(target: Context, key: string | number) {
|
|
48
|
+
return isUnsafeKey(key) && !Object.hasOwn(target, key)
|
|
49
|
+
? undefined
|
|
50
|
+
: target[key]
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/** The key `subject[index]` reads under, or undefined when it reads nothing. */
|
|
48
54
|
function memberKey(index: JexlValue) {
|
|
49
55
|
return typeof index === 'string' || typeof index === 'number'
|
|
@@ -90,23 +96,27 @@ function isKeyPart(
|
|
|
90
96
|
* serves every evaluation.
|
|
91
97
|
*/
|
|
92
98
|
function literalTable(node: AstNode) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
? Object.freeze(literal.value.map((item) => (item as Literal).value))
|
|
99
|
+
if (node.type === 'ArrayLiteral') {
|
|
100
|
+
return node.value.every((item) => item.type === 'Literal')
|
|
101
|
+
? Object.freeze(node.value.map((item) => item.value))
|
|
97
102
|
: undefined
|
|
98
103
|
}
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return Object.freeze(table)
|
|
104
|
+
if (node.type !== 'ObjectLiteral') {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
const table: Context = {}
|
|
108
|
+
for (const [key, value] of Object.entries(node.value)) {
|
|
109
|
+
if (value.type !== 'Literal') {
|
|
110
|
+
return undefined
|
|
107
111
|
}
|
|
112
|
+
defineOwn(table, key, value.value)
|
|
108
113
|
}
|
|
109
|
-
return
|
|
114
|
+
return Object.freeze(table)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function lambda(fn: JexlFunction & { [LAMBDA]?: true }) {
|
|
118
|
+
fn[LAMBDA] = true
|
|
119
|
+
return fn
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
const unassigned = Symbol('unassigned')
|
|
@@ -208,8 +218,7 @@ function climb(hops: number, index: number): CompiledNode {
|
|
|
208
218
|
}
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
function assignedNames(
|
|
212
|
-
const node = ast as AstNodeUnion | undefined
|
|
221
|
+
function assignedNames(node: AstNode | undefined, names = new Set<string>()) {
|
|
213
222
|
switch (node?.type) {
|
|
214
223
|
case 'AssignmentExpression': {
|
|
215
224
|
names.add(node.left.value)
|
|
@@ -335,15 +344,11 @@ function stringify(value: JexlValue) {
|
|
|
335
344
|
}
|
|
336
345
|
|
|
337
346
|
function compileNode(
|
|
338
|
-
|
|
347
|
+
node: AstNode,
|
|
339
348
|
grammar: Grammar,
|
|
340
349
|
scope: Scope
|
|
341
350
|
): CompiledNode {
|
|
342
351
|
const compile = (child: AstNode) => compileNode(child, grammar, scope)
|
|
343
|
-
// AstNode types its `type` as a plain string so the Parser can build the tree
|
|
344
|
-
// loosely; narrowing to the union once, here, lets every case below see its
|
|
345
|
-
// own node type instead of repeating the same cast in each branch
|
|
346
|
-
const node = ast as AstNodeUnion
|
|
347
352
|
switch (node.type) {
|
|
348
353
|
case 'Literal': {
|
|
349
354
|
const { value } = node
|
|
@@ -365,6 +370,12 @@ function compileNode(
|
|
|
365
370
|
: (getMember(target, name) as JexlValue)
|
|
366
371
|
}
|
|
367
372
|
}
|
|
373
|
+
if (isUnsafeKey(name)) {
|
|
374
|
+
return (ctx) => {
|
|
375
|
+
const target = from(ctx)
|
|
376
|
+
return target == null ? undefined : member(target as Context, name)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
368
379
|
return (ctx) => {
|
|
369
380
|
const target = from(ctx)
|
|
370
381
|
return target == null ? undefined : (target as Context)[name]
|
|
@@ -374,7 +385,7 @@ function compileNode(
|
|
|
374
385
|
case 'BinaryExpression': {
|
|
375
386
|
const op = grammar.elements[node.operator]
|
|
376
387
|
const left = compile(node.left)
|
|
377
|
-
const right = compile(node.right
|
|
388
|
+
const right = compile(node.right)
|
|
378
389
|
if (op?.type === 'binaryOp' && op.evalOnDemand) {
|
|
379
390
|
const { evalOnDemand } = op
|
|
380
391
|
if (evalOnDemand === and) {
|
|
@@ -405,7 +416,7 @@ function compileNode(
|
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
case 'UnaryExpression': {
|
|
408
|
-
const right = compile(node.right
|
|
419
|
+
const right = compile(node.right)
|
|
409
420
|
const elem = grammar.elements[node.operator]
|
|
410
421
|
const fn =
|
|
411
422
|
elem?.type === 'unaryOp'
|
|
@@ -452,7 +463,7 @@ function compileNode(
|
|
|
452
463
|
const key = memberKey(index(ctx))
|
|
453
464
|
return subjectVal == null || key === undefined
|
|
454
465
|
? undefined
|
|
455
|
-
: (subjectVal as
|
|
466
|
+
: member(subjectVal as Context, key)
|
|
456
467
|
}
|
|
457
468
|
}
|
|
458
469
|
|
|
@@ -568,7 +579,7 @@ function compileNode(
|
|
|
568
579
|
if (scope.params) {
|
|
569
580
|
throw new Error('Assignment is not supported in a lambda')
|
|
570
581
|
}
|
|
571
|
-
const right = compile(node.right
|
|
582
|
+
const right = compile(node.right)
|
|
572
583
|
const { slots, frame } = scope.locals!
|
|
573
584
|
const slot = slots.get(node.left.value)!
|
|
574
585
|
return (ctx) => (frame.locals[slot] = right(ctx))
|
|
@@ -581,23 +592,25 @@ function compileNode(
|
|
|
581
592
|
})
|
|
582
593
|
if (!scope.params) {
|
|
583
594
|
const frame = scope.locals?.frame
|
|
584
|
-
return (ctx)
|
|
595
|
+
return (ctx) => {
|
|
585
596
|
const locals = frame?.locals
|
|
586
|
-
return (...args) =>
|
|
597
|
+
return lambda((...args) =>
|
|
587
598
|
body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
|
|
599
|
+
)
|
|
588
600
|
}
|
|
589
601
|
}
|
|
590
|
-
return (env)
|
|
602
|
+
return (env) => {
|
|
591
603
|
const outer = asFrame(env)
|
|
592
|
-
return (...args) =>
|
|
604
|
+
return lambda((...args) =>
|
|
593
605
|
body(
|
|
594
606
|
asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
|
|
595
607
|
)
|
|
608
|
+
)
|
|
596
609
|
}
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
default: {
|
|
600
|
-
|
|
613
|
+
return unknownNode(node)
|
|
601
614
|
}
|
|
602
615
|
}
|
|
603
616
|
}
|
package/src/grammar.ts
CHANGED
|
@@ -56,9 +56,9 @@ export interface BinaryOp {
|
|
|
56
56
|
|
|
57
57
|
export type UnaryOpEval = (right: JexlValue) => JexlValue
|
|
58
58
|
|
|
59
|
+
/** A prefix operator, which binds tighter than any binary one. */
|
|
59
60
|
export interface UnaryOp {
|
|
60
61
|
type: 'unaryOp'
|
|
61
|
-
precedence: number
|
|
62
62
|
eval: UnaryOpEval
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -82,6 +82,7 @@ export interface SimpleElement {
|
|
|
82
82
|
| 'question'
|
|
83
83
|
| 'semicolon'
|
|
84
84
|
| 'arrow'
|
|
85
|
+
| 'assign'
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
@@ -174,6 +175,7 @@ export const getGrammar = (): Grammar => ({
|
|
|
174
175
|
'?': { type: 'question' },
|
|
175
176
|
';': { type: 'semicolon' },
|
|
176
177
|
'=>': { type: 'arrow' },
|
|
178
|
+
'=': { type: 'assign' },
|
|
177
179
|
'+': {
|
|
178
180
|
type: 'binaryOp',
|
|
179
181
|
precedence: 30,
|
|
@@ -299,15 +301,7 @@ export const getGrammar = (): Grammar => ({
|
|
|
299
301
|
},
|
|
300
302
|
'!': {
|
|
301
303
|
type: 'unaryOp',
|
|
302
|
-
precedence: Infinity,
|
|
303
304
|
eval: (right) => !right
|
|
304
|
-
},
|
|
305
|
-
'=': {
|
|
306
|
-
type: 'binaryOp',
|
|
307
|
-
precedence: 2,
|
|
308
|
-
eval: (_left, _right) => {
|
|
309
|
-
throw new Error('Assignment handled specially')
|
|
310
|
-
}
|
|
311
305
|
}
|
|
312
306
|
},
|
|
313
307
|
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,8 @@ export type { JexlOptions } from './Jexl.ts'
|
|
|
8
8
|
export type { GetMember, VariableReader } from './grammar.ts'
|
|
9
9
|
export { default as Expression } from './Expression.ts'
|
|
10
10
|
export { analyze } from './analyze.ts'
|
|
11
|
-
export { check,
|
|
11
|
+
export { check, recordOf, union } from './check.ts'
|
|
12
|
+
export { print } from './print.ts'
|
|
12
13
|
export {
|
|
13
14
|
callSubject,
|
|
14
15
|
conditions,
|
package/src/operators.ts
CHANGED
|
@@ -74,20 +74,24 @@ export function pairwise(
|
|
|
74
74
|
return out
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const patterns = new Map<string, RegExp>()
|
|
78
|
-
const MAX_PATTERNS = 1000
|
|
79
|
-
|
|
80
77
|
/**
|
|
81
78
|
* The regular expression a pattern string stands for. A leading `(?i)`, the
|
|
82
79
|
* inline flag PCRE, Python and Go read, makes it case-insensitive.
|
|
83
80
|
*/
|
|
81
|
+
export function toRegExp(pattern: string) {
|
|
82
|
+
return pattern.startsWith('(?i)')
|
|
83
|
+
? new RegExp(pattern.slice(4), 'i')
|
|
84
|
+
: new RegExp(pattern)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const patterns = new Map<string, RegExp>()
|
|
88
|
+
const MAX_PATTERNS = 1000
|
|
89
|
+
|
|
84
90
|
function regex(pattern: string) {
|
|
85
91
|
let re = patterns.get(pattern)
|
|
86
92
|
if (!re) {
|
|
87
93
|
try {
|
|
88
|
-
re = pattern
|
|
89
|
-
? new RegExp(pattern.slice(4), 'i')
|
|
90
|
-
: new RegExp(pattern)
|
|
94
|
+
re = toRegExp(pattern)
|
|
91
95
|
} catch {
|
|
92
96
|
throw new Error(`Invalid regular expression: ${pattern}`)
|
|
93
97
|
}
|
package/src/parser/Parser.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type Lexer from '../Lexer.ts'
|
|
|
9
9
|
import type { Grammar } from '../grammar.ts'
|
|
10
10
|
import type {
|
|
11
11
|
AstNode,
|
|
12
|
-
AstNodeUnion,
|
|
13
12
|
ConditionalExpression,
|
|
14
13
|
Lambda,
|
|
15
14
|
Literal,
|
|
@@ -29,8 +28,6 @@ const omittedAlternateBefore = new Set([
|
|
|
29
28
|
'semicolon'
|
|
30
29
|
])
|
|
31
30
|
|
|
32
|
-
export { JexlSyntaxError } from '../errors.ts'
|
|
33
|
-
|
|
34
31
|
/**
|
|
35
32
|
* Converts the tokens from the {@link Lexer} into an Abstract Syntax Tree, for
|
|
36
33
|
* {@link compileAst} to lower to closures. A Pratt parser: each level of the
|
|
@@ -79,7 +76,7 @@ class Parser {
|
|
|
79
76
|
* @returns the expression tree, or null for an expression with no tokens
|
|
80
77
|
* @throws {JexlSyntaxError} if the tokens do not form exactly one expression
|
|
81
78
|
*/
|
|
82
|
-
complete():
|
|
79
|
+
complete(): AstNode | null {
|
|
83
80
|
if (this._tokens.length === 0) {
|
|
84
81
|
return null
|
|
85
82
|
}
|
|
@@ -90,7 +87,7 @@ class Parser {
|
|
|
90
87
|
return ast
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
_sequence():
|
|
90
|
+
_sequence(): AstNode {
|
|
94
91
|
const first = this._assignment()
|
|
95
92
|
if (this._peek()?.type !== 'semicolon') {
|
|
96
93
|
return first
|
|
@@ -106,13 +103,12 @@ class Parser {
|
|
|
106
103
|
return { type: 'SequenceExpression', expressions }
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
_assignment():
|
|
106
|
+
_assignment(): AstNode {
|
|
110
107
|
if (this._atLambda()) {
|
|
111
108
|
return this._lambda()
|
|
112
109
|
}
|
|
113
110
|
const left = this._ternary()
|
|
114
|
-
|
|
115
|
-
if (token?.type !== 'binaryOp' || token.value !== '=') {
|
|
111
|
+
if (this._peek()?.type !== 'assign') {
|
|
116
112
|
return left
|
|
117
113
|
}
|
|
118
114
|
if (left.type !== 'Identifier' || left.from) {
|
|
@@ -176,7 +172,7 @@ class Parser {
|
|
|
176
172
|
return { type: 'Lambda', params, body }
|
|
177
173
|
}
|
|
178
174
|
|
|
179
|
-
_ternary():
|
|
175
|
+
_ternary(): AstNode {
|
|
180
176
|
const test = this._binary(-Infinity)
|
|
181
177
|
if (!this._eat('question')) {
|
|
182
178
|
return test
|
|
@@ -198,11 +194,11 @@ class Parser {
|
|
|
198
194
|
* One exactly at `floor` binds only when it continues a right-associative
|
|
199
195
|
* chain, which is how `a ^ b ^ c` groups from the right.
|
|
200
196
|
*/
|
|
201
|
-
_binary(floor: number, rightAssociative = false):
|
|
197
|
+
_binary(floor: number, rightAssociative = false): AstNode {
|
|
202
198
|
let left = this._unary()
|
|
203
199
|
for (;;) {
|
|
204
200
|
const token = this._peek()
|
|
205
|
-
if (token?.type !== 'binaryOp'
|
|
201
|
+
if (token?.type !== 'binaryOp') {
|
|
206
202
|
return left
|
|
207
203
|
}
|
|
208
204
|
const operator = token.value as string
|
|
@@ -224,7 +220,7 @@ class Parser {
|
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
/** `a ?? b || c` has no grouping a reader can guess; JS refuses it too. */
|
|
227
|
-
_mixesNullish(operator: string, ...operands:
|
|
223
|
+
_mixesNullish(operator: string, ...operands: AstNode[]) {
|
|
228
224
|
return operands.some(
|
|
229
225
|
(operand) =>
|
|
230
226
|
operand.type === 'BinaryExpression' &&
|
|
@@ -235,22 +231,20 @@ class Parser {
|
|
|
235
231
|
)
|
|
236
232
|
}
|
|
237
233
|
|
|
238
|
-
_unary():
|
|
234
|
+
_unary(): AstNode {
|
|
239
235
|
const token = this._peek()
|
|
240
236
|
if (token?.type !== 'unaryOp') {
|
|
241
237
|
return this._postfix(this._primary())
|
|
242
238
|
}
|
|
243
239
|
this._pos++
|
|
244
|
-
const operator = token.value as string
|
|
245
|
-
const op = this._grammar.elements[operator]
|
|
246
240
|
return {
|
|
247
241
|
type: 'UnaryExpression',
|
|
248
|
-
operator,
|
|
249
|
-
right: this.
|
|
242
|
+
operator: token.value as string,
|
|
243
|
+
right: this._unary()
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
|
|
253
|
-
_postfix(subject:
|
|
247
|
+
_postfix(subject: AstNode): AstNode {
|
|
254
248
|
let node = subject
|
|
255
249
|
for (;;) {
|
|
256
250
|
switch (this._peek()?.type) {
|
|
@@ -294,7 +288,7 @@ class Parser {
|
|
|
294
288
|
}
|
|
295
289
|
}
|
|
296
290
|
|
|
297
|
-
_primary():
|
|
291
|
+
_primary(): AstNode {
|
|
298
292
|
const token = this._next()
|
|
299
293
|
switch (token.type) {
|
|
300
294
|
case 'literal': {
|
package/src/print.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { identRegex } from './Lexer.ts'
|
|
7
|
+
import { getGrammar } from './grammar.ts'
|
|
8
|
+
|
|
9
|
+
import type { AstNode, BinaryExpression } from './types.ts'
|
|
10
|
+
|
|
11
|
+
const { elements } = getGrammar()
|
|
12
|
+
|
|
13
|
+
// print knows where only the built-in operators bind, so a host's own ones
|
|
14
|
+
// print with their operands parenthesized
|
|
15
|
+
function builtIn(operator: string) {
|
|
16
|
+
const element = elements[operator]
|
|
17
|
+
return element?.type === 'binaryOp' ? element : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LOGICAL = new Set(['||', '&&'])
|
|
21
|
+
|
|
22
|
+
// how tightly each form binds, mirroring the Parser's levels: `;`, then `=`
|
|
23
|
+
// and lambdas, then `?:`, then binary operators, then prefix operators
|
|
24
|
+
const SEQUENCE = 0
|
|
25
|
+
const ASSIGNMENT = 1
|
|
26
|
+
const CONDITIONAL = 2
|
|
27
|
+
const CUSTOM_BINARY = 3
|
|
28
|
+
const UNARY = 100
|
|
29
|
+
|
|
30
|
+
const WORDS = new Set([
|
|
31
|
+
'true',
|
|
32
|
+
'false',
|
|
33
|
+
'null',
|
|
34
|
+
...Object.keys(elements).filter((word) => identRegex.test(word))
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
export function isName(text: string) {
|
|
38
|
+
return identRegex.test(text) && !WORDS.has(text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function key(name: string) {
|
|
42
|
+
return isName(name) ? `.${name}` : `[${quote(name)}]`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function quote(text: string) {
|
|
46
|
+
return `'${text.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function precedenceOf(node: AstNode) {
|
|
50
|
+
switch (node.type) {
|
|
51
|
+
case 'SequenceExpression': {
|
|
52
|
+
return SEQUENCE
|
|
53
|
+
}
|
|
54
|
+
case 'AssignmentExpression':
|
|
55
|
+
case 'Lambda': {
|
|
56
|
+
return ASSIGNMENT
|
|
57
|
+
}
|
|
58
|
+
case 'ConditionalExpression': {
|
|
59
|
+
// with no alternate, it ends only before `)`, `]`, `}`, `,`, `;` or the end
|
|
60
|
+
return node.alternate ? CONDITIONAL : SEQUENCE
|
|
61
|
+
}
|
|
62
|
+
case 'BinaryExpression': {
|
|
63
|
+
return builtIn(node.operator)?.precedence ?? CUSTOM_BINARY
|
|
64
|
+
}
|
|
65
|
+
case 'UnaryExpression': {
|
|
66
|
+
return UNARY
|
|
67
|
+
}
|
|
68
|
+
default: {
|
|
69
|
+
return Infinity
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function within(node: AstNode, least: number) {
|
|
75
|
+
const text = print(node)
|
|
76
|
+
return precedenceOf(node) < least ? `(${text})` : text
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function binaryOperand(
|
|
80
|
+
parent: BinaryExpression,
|
|
81
|
+
child: AstNode,
|
|
82
|
+
left: boolean
|
|
83
|
+
) {
|
|
84
|
+
const op = builtIn(parent.operator)
|
|
85
|
+
if (!op) {
|
|
86
|
+
return within(child, UNARY)
|
|
87
|
+
}
|
|
88
|
+
const own = op.precedence
|
|
89
|
+
const theirs = precedenceOf(child)
|
|
90
|
+
const chained =
|
|
91
|
+
child.type === 'BinaryExpression' &&
|
|
92
|
+
!!op.rightAssociative &&
|
|
93
|
+
!!builtIn(child.operator)?.rightAssociative
|
|
94
|
+
const mixesNullish =
|
|
95
|
+
child.type === 'BinaryExpression' &&
|
|
96
|
+
((parent.operator === '??' && LOGICAL.has(child.operator)) ||
|
|
97
|
+
(child.operator === '??' && LOGICAL.has(parent.operator)))
|
|
98
|
+
const text = print(child)
|
|
99
|
+
return theirs < own || (theirs === own && left === chained) || mixesNullish
|
|
100
|
+
? `(${text})`
|
|
101
|
+
: text
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Renders a tree back to expression text, for messages and suggestions. The
|
|
106
|
+
* text parses back to the same tree.
|
|
107
|
+
*/
|
|
108
|
+
export function print(node: AstNode): string {
|
|
109
|
+
switch (node.type) {
|
|
110
|
+
case 'Literal': {
|
|
111
|
+
const { value } = node
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
return quote(value)
|
|
114
|
+
}
|
|
115
|
+
// a number too large for a double reads as Infinity, which is a name
|
|
116
|
+
return Object.is(value, -0)
|
|
117
|
+
? '-0'
|
|
118
|
+
: value === Infinity
|
|
119
|
+
? '1e999'
|
|
120
|
+
: value === -Infinity
|
|
121
|
+
? '-1e999'
|
|
122
|
+
: String(value)
|
|
123
|
+
}
|
|
124
|
+
case 'Identifier': {
|
|
125
|
+
return node.from
|
|
126
|
+
? within(node.from, Infinity) + key(node.value)
|
|
127
|
+
: node.value
|
|
128
|
+
}
|
|
129
|
+
case 'FilterExpression': {
|
|
130
|
+
return `${within(node.subject, Infinity)}[${within(node.expr, ASSIGNMENT)}]`
|
|
131
|
+
}
|
|
132
|
+
case 'BinaryExpression': {
|
|
133
|
+
return `${binaryOperand(node, node.left, true)} ${node.operator} ${binaryOperand(node, node.right, false)}`
|
|
134
|
+
}
|
|
135
|
+
case 'UnaryExpression': {
|
|
136
|
+
const { operator } = node
|
|
137
|
+
const right = within(node.right, UNARY)
|
|
138
|
+
// the Lexer folds a minus before a digit into the number
|
|
139
|
+
if (operator === '-' && /^\d/.test(right)) {
|
|
140
|
+
return `-(${right})`
|
|
141
|
+
}
|
|
142
|
+
return identRegex.test(operator)
|
|
143
|
+
? `${operator} ${right}`
|
|
144
|
+
: operator + right
|
|
145
|
+
}
|
|
146
|
+
case 'ConditionalExpression': {
|
|
147
|
+
const test = within(node.test, CUSTOM_BINARY)
|
|
148
|
+
const consequent = node.consequent
|
|
149
|
+
? ` ${within(node.consequent, ASSIGNMENT)} `
|
|
150
|
+
: ''
|
|
151
|
+
const alternate = node.alternate
|
|
152
|
+
? ` ${within(node.alternate, ASSIGNMENT)}`
|
|
153
|
+
: ''
|
|
154
|
+
return `${test} ?${consequent}:${alternate}`
|
|
155
|
+
}
|
|
156
|
+
case 'FunctionCall': {
|
|
157
|
+
const args = node.args.map((arg) => within(arg, ASSIGNMENT))
|
|
158
|
+
return `${node.name}(${args.join(', ')})`
|
|
159
|
+
}
|
|
160
|
+
case 'ArrayLiteral': {
|
|
161
|
+
const items = node.value.map((item) => within(item, ASSIGNMENT))
|
|
162
|
+
return `[${items.join(', ')}]`
|
|
163
|
+
}
|
|
164
|
+
case 'ObjectLiteral': {
|
|
165
|
+
const entries = Object.entries(node.value).map(
|
|
166
|
+
([name, value]) =>
|
|
167
|
+
`${isName(name) ? name : quote(name)}: ${within(value, ASSIGNMENT)}`
|
|
168
|
+
)
|
|
169
|
+
return `{${entries.join(', ')}}`
|
|
170
|
+
}
|
|
171
|
+
case 'TemplateLiteral': {
|
|
172
|
+
const parts = node.parts.map((part) =>
|
|
173
|
+
part.type === 'static'
|
|
174
|
+
? part.value
|
|
175
|
+
.replaceAll('\\', '\\\\')
|
|
176
|
+
.replaceAll('`', '\\`')
|
|
177
|
+
.replaceAll('${', '\\${')
|
|
178
|
+
: `\${${print(part.value)}}`
|
|
179
|
+
)
|
|
180
|
+
return `\`${parts.join('')}\``
|
|
181
|
+
}
|
|
182
|
+
case 'SequenceExpression': {
|
|
183
|
+
const { expressions } = node
|
|
184
|
+
const text = expressions
|
|
185
|
+
.map((expr) => within(expr, ASSIGNMENT))
|
|
186
|
+
.join('; ')
|
|
187
|
+
return expressions.length === 1 ? `${text};` : text
|
|
188
|
+
}
|
|
189
|
+
case 'AssignmentExpression': {
|
|
190
|
+
return `${node.left.value} = ${within(node.right, ASSIGNMENT)}`
|
|
191
|
+
}
|
|
192
|
+
case 'Lambda': {
|
|
193
|
+
const params =
|
|
194
|
+
node.params.length === 1
|
|
195
|
+
? node.params[0]
|
|
196
|
+
: `(${node.params.join(', ')})`
|
|
197
|
+
return `${params} => ${within(node.body, ASSIGNMENT)}`
|
|
198
|
+
}
|
|
199
|
+
default: {
|
|
200
|
+
return '?'
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|