@tabnas/expr 0.2.1 → 0.2.3

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/src/expr.ts ADDED
@@ -0,0 +1,1475 @@
1
+ /* Copyright (c) 2021-2025 Richard Rodger, MIT License */
2
+
3
+ // This algorithm is based on Pratt parsing, and draws heavily from
4
+ // the explanation written by Aleksey Kladov here:
5
+ // https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
6
+ // See the `prattify` function for the core implementation.
7
+ //
8
+ // Expressions are encoded as LISP-style S-expressions using
9
+ // arrays. The operation meta data is provided as the first array
10
+ // element. To maintain the integrity of the overall JSON AST,
11
+ // expression rules cannot simply re-assign nodes. Instead the
12
+ // existing partial expression nodes are rewritten in-place.
13
+ //
14
+ // Parentheses can have preceeding values, which allows for the using function
15
+ // call ("foo(1)") and index ("a[1]") syntax. See the tests for examples and
16
+ // configuration options.
17
+ //
18
+ // Ternary expressions are implemented as special rule that is similar to
19
+ // the parenthesis rule. You can have multiple ternaries.
20
+ //
21
+ // Standard Jsonic allows for implicit lists and maps (e.g. a,b =>
22
+ // ['a','b']) at the top level. This expression grammar also allows
23
+ // for implicits within parentheses, so that "foo(1,2)" =>
24
+ // ['(','foo',[1,2]]. To support implicits additional counters and
25
+ // flags are needed, as well as context-sensitive edge-case
26
+ // handling. See the ternary rule for a glorious example.
27
+ //
28
+ // There is a specific recurring edge-case: when expressions are the
29
+ // first item of a list, special care is need not to embed the list
30
+ // inside the expression.
31
+
32
+ // TODO: custom ctx.F for Op - make this automatic in options
33
+ // TODO: error on incomplete expr: 1+2+
34
+
35
+ // The engine is the tabnas parser; jsonic supplies the relaxed-JSON
36
+ // grammar. Engine types are re-exported by @tabnas/parser.
37
+ import {
38
+ Tabnas,
39
+ Plugin,
40
+ Rule,
41
+ RuleSpec,
42
+ AltSpec,
43
+ AltMatch,
44
+ Tin,
45
+ Context,
46
+ Token,
47
+ util,
48
+ } from '@tabnas/parser'
49
+
50
+ const { omap, entries, values } = util
51
+
52
+ // Operator definition (value of options.op map entry).
53
+ type OpDef = {
54
+ src?: string | string[]
55
+ osrc?: string
56
+ csrc?: string
57
+ left?: number
58
+ right?: number
59
+ use?: any // custom op data
60
+ prefix?: boolean
61
+ suffix?: boolean
62
+ infix?: boolean
63
+ ternary?: boolean
64
+ paren?: boolean
65
+ preval?: {
66
+ active?: boolean
67
+ required?: boolean
68
+ allow?: string[]
69
+ }
70
+ }
71
+
72
+ // Options for the plugin.
73
+ type ExprOptions = {
74
+ op?: { [name: string]: OpDef }
75
+
76
+ // TODO: define Evalute type
77
+ evaluate?: typeof evaluation
78
+ }
79
+
80
+ // Full operator description (provided for evaluation).
81
+ type Op = {
82
+ name: string
83
+ src: string
84
+ left: number
85
+ right: number
86
+ use: any
87
+ prefix: boolean
88
+ suffix: boolean
89
+ infix: boolean
90
+ ternary: boolean
91
+ paren: boolean
92
+ terms: number
93
+ tkn: string
94
+ tin: number
95
+ osrc: string
96
+ csrc: string
97
+ otkn: string
98
+ otin: number
99
+ ctkn: string
100
+ ctin: number
101
+ preval: {
102
+ active: boolean
103
+ required: boolean
104
+ allow?: string[]
105
+ }
106
+ token: Token
107
+ OP_MARK: typeof OP_MARK
108
+ }
109
+
110
+ // Lookup operators by token.
111
+ type OpMap = { [tin: number]: Op }
112
+
113
+ // Resolve the value of an operartion
114
+ type Evaluate = (rule: Rule, ctx: Context, op: Op, ...terms: any) => any
115
+
116
+ // Mark Operator objects as owned by this plugin.
117
+ const OP_MARK = {}
118
+
119
+ // The plugin itself.
120
+ let Expr: Plugin = function Expr(tn: Tabnas, options: ExprOptions) {
121
+ // Ensure comment matcher is first to avoid conflicts with
122
+ // comment markers (//, /*, etc)
123
+ // let lexm = jsonic.options.lex?.match || []
124
+ // let cmI: number = lexm.map((m) => m.name).indexOf('makeCommentMatcher')
125
+ // if (0 < cmI) {
126
+ // jsonic.options({
127
+ // lex: {
128
+ // match: [lexm[cmI], ...lexm.slice(0, cmI), ...lexm.slice(cmI + 1)],
129
+ // },
130
+ // })
131
+ // }
132
+
133
+ // console.log('EXPR', options)
134
+
135
+ let token = tn.token.bind(tn) as any
136
+ let fixed = tn.fixed.bind(tn) as any
137
+
138
+ // Build token maps (TM).
139
+ let optop = options.op || {}
140
+
141
+ // Delete operations marked null.
142
+ for (let opname in optop) {
143
+ if (null === optop[opname]) {
144
+ delete optop[opname]
145
+ }
146
+ }
147
+
148
+ const prefixTM: OpMap = makeOpMap(token, fixed, optop, 'prefix')
149
+ const suffixTM: OpMap = makeOpMap(token, fixed, optop, 'suffix')
150
+ const infixTM: OpMap = makeOpMap(token, fixed, optop, 'infix')
151
+ const ternaryTM: OpMap = makeOpMap(token, fixed, optop, 'ternary')
152
+
153
+ const parenOTM: OpMap = makeParenMap(token, fixed, optop)
154
+ const parenCTM: OpMap = omap(parenOTM, ([_, pdef]: [Tin, Op]) => [
155
+ undefined,
156
+ undefined,
157
+ pdef.ctin,
158
+ pdef,
159
+ ])
160
+
161
+ let parenFixed = Object.values({ ...parenOTM, ...parenCTM }).reduce(
162
+ (a, p) => ((a[p.otkn] = p.osrc), (a[p.ctkn] = p.csrc), a),
163
+ {} as any,
164
+ )
165
+
166
+ // NOTE: operators with same src will generate same token - this is correct.
167
+ let operatorFixed = Object.values({
168
+ ...prefixTM,
169
+ ...suffixTM,
170
+ ...infixTM,
171
+ ...ternaryTM,
172
+ }).reduce((a, op) => ((a[op.tkn] = op.src), a), {} as any)
173
+
174
+ tn.options({
175
+ fixed: {
176
+ token: { ...operatorFixed, ...parenFixed },
177
+ },
178
+
179
+ lex: {
180
+ match: {
181
+ comment: { order: 1e5 },
182
+ },
183
+ },
184
+ })
185
+
186
+ // Append 'expr' to the g (group tag) of every alt added below. Mirrors
187
+ // the jsonic grammar(...) setting {rule:{alt:{g:'expr'}}}, applied
188
+ // manually because the plugin uses jsonic.rule() (not jsonic.grammar()).
189
+ const tagExpr = (alts: any): any => {
190
+ if (!Array.isArray(alts)) return alts
191
+ return alts.map((a) => {
192
+ if (null == a || 'object' !== typeof a) return a
193
+ const existing = null == a.g
194
+ ? []
195
+ : Array.isArray(a.g)
196
+ ? [...a.g]
197
+ : String(a.g).split(/\s*,\s*/).filter((s: string) => s.length > 0)
198
+ return { ...a, g: [...existing, 'expr'] }
199
+ })
200
+ }
201
+
202
+ const rule = (name: string, fn: (rs: RuleSpec) => void) => {
203
+ tn.rule(name, (rs: RuleSpec) => {
204
+ const origOpen = rs.open.bind(rs)
205
+ const origClose = rs.close.bind(rs)
206
+ ;(rs as any).open = (alts: any, flags?: any) => origOpen(tagExpr(alts), flags)
207
+ ;(rs as any).close = (alts: any, flags?: any) => origClose(tagExpr(alts), flags)
208
+ fn(rs)
209
+ })
210
+ }
211
+
212
+ const PREFIX = values(prefixTM).map((op: any) => op.tin)
213
+ const INFIX = values(infixTM).map((op: any) => op.tin)
214
+ const SUFFIX = values(suffixTM).map((op: any) => op.tin)
215
+
216
+ const TERN0 = values(ternaryTM)
217
+ .filter((op: any) => 0 === op.use.ternary.opI)
218
+ .map((op: any) => op.tin)
219
+ const TERN1 = values(ternaryTM)
220
+ .filter((op: any) => 1 === op.use.ternary.opI)
221
+ .map((op: any) => op.tin)
222
+
223
+ const OP = values(parenOTM).map((pdef: any) => pdef.otin)
224
+ const CP = values(parenCTM).map((pdef: any) => pdef.ctin)
225
+
226
+ const hasPrefix = 0 < PREFIX.length
227
+ const hasInfix = 0 < INFIX.length
228
+ const hasSuffix = 0 < SUFFIX.length
229
+ const hasTernary = 0 < TERN0.length && 0 < TERN1.length
230
+ const hasParen = 0 < OP.length && 0 < CP.length
231
+
232
+ const CA = tn.token.CA
233
+ const CS = tn.token.CS
234
+ const CB = tn.token.CB
235
+ const TX = tn.token.TX
236
+ const NR = tn.token.NR
237
+ const ST = tn.token.ST
238
+ const VL = tn.token.VL
239
+ const ZZ = tn.token.ZZ
240
+
241
+ const VAL = [TX, NR, ST, VL]
242
+
243
+ const NONE = null as unknown as AltSpec
244
+
245
+ rule('val', (rs: RuleSpec) => {
246
+ // TODO: jsonic - make it easier to handle this case
247
+ // Implicit pair not allowed inside ternary
248
+ if (hasTernary && TERN1.includes(tn.token.CL)) {
249
+ rs.def.open
250
+ .filter((a: any) => a.g.includes('pair'))
251
+ .map((alt: any) => {
252
+ let origcond = alt.c
253
+ let internary = (r: Rule) => !r.n.expr_ternary
254
+ alt.c = origcond
255
+ ? (r: Rule, ctx: Context) => origcond(r, ctx) && internary(r)
256
+ : internary
257
+ })
258
+ }
259
+
260
+ rs.open([
261
+ // The prefix operator of the first term of an expression.
262
+ hasPrefix
263
+ ? {
264
+ s: [PREFIX],
265
+ b: 1,
266
+ n: { expr_prefix: 1, expr_suffix: 0 },
267
+ p: 'expr',
268
+ g: 'expr,expr-prefix',
269
+ }
270
+ : NONE,
271
+
272
+ // WWW
273
+ hasParen
274
+ ? {
275
+ s: [VAL, OP],
276
+ b: 1,
277
+ p: 'expr',
278
+ c: (r: Rule, ctx: Context) => {
279
+ const pdef = parenOTM[r.o1.tin]
280
+ const preval = r.o0.resolveVal(r, ctx)
281
+ return pdef.preval.active &&
282
+ (null == pdef.preval.allow || pdef.preval.allow.includes(preval))
283
+ },
284
+ u: { paren_preval: true },
285
+ g: 'expr,expr-paren,expr-paren-preval',
286
+ a: (r: Rule, ctx: Context) => {
287
+ r.node = r.o0.resolveVal(r, ctx)
288
+ }
289
+ }
290
+ : NONE,
291
+
292
+ // An opening parenthesis.
293
+ // NOTE: this can happen outside an expression.
294
+ hasParen
295
+ ? {
296
+ s: [OP],
297
+ b: 1,
298
+ p: 'expr',
299
+
300
+ c: (r: Rule) => {
301
+ const pdef = parenOTM[r.o0.tin]
302
+ return !pdef.preval.required
303
+ },
304
+
305
+ g: 'expr,expr-paren',
306
+ }
307
+ : NONE,
308
+ ]).close([
309
+ // Comma-op suppression. When a parent rule (e.g. an embedding
310
+ // grammar's wrapper rule) sets n.no_comma_op, bail at `,` without
311
+ // treating it as the comma operator — the parent then consumes
312
+ // the `,` itself as a separator. Match by `src` on the next infix
313
+ // token so this works regardless of which token the embedding
314
+ // grammar uses for `,`.
315
+ hasInfix
316
+ ? {
317
+ s: [INFIX],
318
+ c: (r: Rule) =>
319
+ 0 < (r.n.no_comma_op || 0) && r.c0?.src === ',',
320
+ b: 1,
321
+ g: 'expr,no-comma-op-bail',
322
+ }
323
+ : NONE,
324
+
325
+ hasTernary
326
+ ? {
327
+ s: [TERN0],
328
+ c: (r: Rule) => !r.n.expr,
329
+ b: 1,
330
+ r: 'ternary',
331
+ g: 'expr,expr-ternary',
332
+ }
333
+ : NONE,
334
+
335
+ // The infix operator following the first term of an expression.
336
+ hasInfix
337
+ ? {
338
+ s: [INFIX],
339
+ b: 1,
340
+ n: { expr_prefix: 0, expr_suffix: 0 },
341
+ r: (r: Rule) => (!r.n.expr ? 'expr' : ''),
342
+ g: 'expr,expr-infix',
343
+ }
344
+ : NONE,
345
+
346
+ // The suffix operator following the first term of an expression.
347
+ hasSuffix
348
+ ? {
349
+ s: [SUFFIX],
350
+ b: 1,
351
+ n: { expr_prefix: 0, expr_suffix: 1 },
352
+ r: (r: Rule) => (!r.n.expr ? 'expr' : ''),
353
+ g: 'expr,expr-suffix',
354
+ }
355
+ : NONE,
356
+
357
+ // The closing parenthesis of an expression.
358
+ hasParen
359
+ ? {
360
+ s: [CP],
361
+ c: (r: Rule) => !!r.n.expr_paren,
362
+ b: 1,
363
+ g: 'expr,expr-paren',
364
+ }
365
+ : NONE,
366
+
367
+ // Chain for postfix paren forms. When a val has just produced a
368
+ // value (e.g. `a[0]`, `f(0)`, or a parenthesised expression like
369
+ // `(*p)`) and the next token is another preval-active paren-open,
370
+ // push expr (which descends into paren) so the new paren-form
371
+ // picks up this val's node as the preval. Use `p: 'expr'` (not
372
+ // `r: 'val'`) so the current val rule stays alive and
373
+ // `ctx.root().node` still reflects the chained result on parser
374
+ // return. `u: { paren_preval: true }` is set so makeCloseParen
375
+ // finds it on r.parent.parent (= this val) and pushes our node
376
+ // into the new paren CST. This complements the open-time
377
+ // s:[VAL,OP] preval alt above, which only fires on the first
378
+ // preval-paren of an expression; chain-time detection is needed
379
+ // for subsequent parens because the leading "value" is by then a
380
+ // produced node, not a token in the lex buffer.
381
+ hasParen
382
+ ? {
383
+ s: [OP],
384
+ b: 1,
385
+ c: (r: Rule) => {
386
+ const pdef = parenOTM[r.c0.tin]
387
+ return pdef.preval.active &&
388
+ undefined !== r.node &&
389
+ (null == pdef.preval.allow || pdef.preval.allow.includes(r.node))
390
+ },
391
+ p: 'expr',
392
+ u: { paren_preval: true },
393
+ g: 'expr,expr-paren,expr-paren-preval-chain',
394
+ }
395
+ : NONE,
396
+
397
+ hasTernary
398
+ ? {
399
+ s: [TERN1],
400
+ c: (r: Rule) => !!r.n.expr_ternary,
401
+ b: 1,
402
+ g: 'expr,expr-ternary',
403
+ }
404
+ : NONE,
405
+
406
+ // Don't create implicit list inside expression (comma separator).
407
+ {
408
+ s: [CA],
409
+ c: (r: Rule) =>
410
+ (1 === r.d && (1 <= r.n.expr || 1 <= r.n.expr_ternary)) ||
411
+ (1 <= r.n.expr_ternary && 1 <= r.n.expr_paren),
412
+ b: 1,
413
+ g: 'expr,list,val,imp,comma,top',
414
+ },
415
+
416
+ // Don't create implicit list inside expression (space separator).
417
+ {
418
+ s: [VAL],
419
+ c: (r: Rule) =>
420
+ (1 === r.d && (1 <= r.n.expr || 1 <= r.n.expr_ternary)) ||
421
+ (1 <= r.n.expr_ternary && 1 <= r.n.expr_paren),
422
+ b: 1,
423
+ g: 'expr,list,val,imp,space,top',
424
+ },
425
+ ])
426
+ })
427
+
428
+
429
+ rule('list', (rs: RuleSpec) => {
430
+ // rs.bo(false, (...rest: any) => {
431
+ rs.bo(false, (r: Rule) => {
432
+ // List elements are new expressions.
433
+ // Unless this is an implicit list.
434
+ if (!r.prev.u.implist) {
435
+ r.n.expr = 0
436
+ r.n.expr_prefix = 0
437
+ r.n.expr_suffix = 0
438
+ r.n.expr_paren = 0
439
+ r.n.expr_ternary = 0
440
+ }
441
+ }).close([
442
+ hasParen && {
443
+ s: [CP],
444
+
445
+ // If end of normal list, consume `]` - it's not a close paren.
446
+ b: (r: Rule) => (CS === r.c0.tin && !r.n.expr_paren ? 0 : 1),
447
+ },
448
+ ])
449
+ })
450
+
451
+ rule('map', (rs: RuleSpec) => {
452
+ rs.bo(false, (...rest: any) => {
453
+ // Map values are new expressions.
454
+ rest[0].n.expr = 0
455
+ rest[0].n.expr_prefix = 0
456
+ rest[0].n.expr_suffix = 0
457
+ rest[0].n.expr_paren = 0
458
+ rest[0].n.expr_ternary = 0
459
+ }).close([
460
+ hasParen && {
461
+ s: [CP],
462
+ // If end of normal map, consume `}` - it's not a close paren.
463
+ b: (r: Rule) => (CB === r.c0.tin && !r.n.expr_paren ? 0 : 1),
464
+ },
465
+ ])
466
+ })
467
+
468
+ rule('elem', (rs: RuleSpec) => {
469
+ rs.close([
470
+ // Close implicit list within parens.
471
+ hasParen
472
+ ? {
473
+ s: [CP],
474
+ b: 1,
475
+ c: (r: Rule) => !!r.n.expr_paren,
476
+ g: 'expr,expr-paren,imp,close,list',
477
+ }
478
+ : NONE,
479
+
480
+ // Following elem is a paren expression.
481
+ hasParen
482
+ ? {
483
+ s: [OP],
484
+ b: 1,
485
+ r: 'elem',
486
+ g: 'expr,expr-paren,imp,open,list',
487
+ }
488
+ : NONE,
489
+ ])
490
+ })
491
+
492
+ rule('pair', (rs: RuleSpec) => {
493
+ rs.close([
494
+ // Close implicit map within parens.
495
+ hasParen
496
+ ? {
497
+ s: [CP],
498
+ b: 1,
499
+ c: (r: Rule) => !!r.n.expr_paren || 0 < r.n.pk,
500
+ g: 'expr,expr-paren,imp,map',
501
+ }
502
+ : NONE,
503
+ ])
504
+ })
505
+
506
+ rule('expr', (rs: RuleSpec) => {
507
+ rs.open([
508
+
509
+ // An opening parenthesis of an expression.
510
+ hasParen
511
+ ? {
512
+ s: [OP],
513
+ p: 'paren',
514
+ b: 1,
515
+
516
+ g: 'expr,expr-paren,expr-start',
517
+ }
518
+ : NONE,
519
+
520
+ hasPrefix
521
+ ? {
522
+ s: [PREFIX],
523
+ c: (r: Rule) => !!r.n.expr_prefix,
524
+ n: { expr: 1, dlist: 1, dmap: 1 },
525
+ p: 'val',
526
+ g: 'expr,expr-prefix',
527
+ a: (r: Rule) => {
528
+ const op = makeOp(r.o0, prefixTM)
529
+ // Only fold into the parent's node when it is an expression
530
+ // op THIS prefix chain is building (e.g. the outer `-` of
531
+ // `--1`). A ternary/paren op-array on the parent is a foreign
532
+ // seed the engine threaded down by parent-seeding — prattify
533
+ // would splice the new prefix into the wrapper (corrupting it
534
+ // with a stray hole + duplicated operand). Start a fresh
535
+ // operand expression for those (mirrors the infix guard).
536
+ r.node = isOp(r.parent.node)
537
+ && !isTernaryOp(r.parent.node)
538
+ && !isParenOp(r.parent.node)
539
+ ? prattify(r.parent.node, op, 'expr-prefix')
540
+ : prior(r, r.parent, op, 'expr-prefix')
541
+ },
542
+ }
543
+ : NONE,
544
+
545
+ hasInfix
546
+ ? {
547
+ s: [INFIX],
548
+ p: 'val',
549
+ n: { expr: 1, expr_prefix: 0, dlist: 1, dmap: 1 },
550
+ a: (r: Rule) => {
551
+ const prev = r.prev
552
+ const parent = r.parent
553
+ const op = makeOp(r.o0, infixTM)
554
+
555
+ // Second and further operators.
556
+ if (isOp(parent.node) && !isTernaryOp(parent.node)) {
557
+ // console.log('INFIX-A', r.i, p(r.node), parent.i, p(parent.node))
558
+ r.node = prattify(parent.node, op, 'expr-infix-more')
559
+ // console.log('INFIX-B', r.i, p(r.node), parent.i, p(parent.node))
560
+ }
561
+
562
+ // First term was unary expression.
563
+ else if (isOp(prev.node)) {
564
+ r.node = prattify(prev.node, op, 'expr-infix-unary')
565
+ r.parent = prev
566
+ }
567
+
568
+ // First term was plain value or ternary part.
569
+ else {
570
+ r.node = prior(r, prev, op, 'expr-infix')
571
+ }
572
+ },
573
+ g: 'expr,expr-infix',
574
+ }
575
+ : NONE,
576
+
577
+ hasSuffix
578
+ ? {
579
+ s: [SUFFIX],
580
+ n: { expr: 1, expr_prefix: 0, dlist: 1, dmap: 1 },
581
+ a: (r: Rule) => {
582
+ const prev = r.prev
583
+ const op = makeOp(r.o0, suffixTM)
584
+ r.node = isOp(prev.node)
585
+ ? prattify(prev.node, op, 'expr-suffix')
586
+ : prior(r, prev, op, 'expr-suffix')
587
+ },
588
+ g: 'expr,expr-suffix',
589
+ }
590
+ : NONE,
591
+ ])
592
+
593
+ .bc((r: Rule) => {
594
+ const addterm =
595
+ isOp(r.node)
596
+ && r.node?.length - 1 < r.node[0].terms
597
+ && ('object' !== typeof r.node || r.node !== r.child.node)
598
+
599
+ // Append final term to expression.
600
+ if (addterm) {
601
+ r.node.push(r.child.node)
602
+ }
603
+ })
604
+
605
+ .close([
606
+ {
607
+ c: (r: Rule) => 'paren' === r.child.name,
608
+ n: { expr: 0 },
609
+ g: 'expr,expr-end,expr-paren-end',
610
+ },
611
+
612
+ // Comma-op suppression. When n.no_comma_op is set by a parent
613
+ // rule, terminate the expression at `,` without treating it as
614
+ // the comma operator (mirrors the val.close bail above for
615
+ // expressions that have already passed the first term). Match
616
+ // by `src` on the next infix token. n: { expr: 0 } closes the
617
+ // expression frame so the parent rule regains control before
618
+ // any comma-op INFIX alt below fires.
619
+ hasInfix
620
+ ? {
621
+ s: [INFIX],
622
+ c: (r: Rule) =>
623
+ 0 < (r.n.no_comma_op || 0) && r.c0?.src === ',',
624
+ b: 1,
625
+ n: { expr: 0 },
626
+ g: 'expr,no-comma-op-bail',
627
+ }
628
+ : NONE,
629
+
630
+ hasInfix
631
+ ? {
632
+ s: [INFIX],
633
+ // Complete prefix first.
634
+ c: (r: Rule) => !r.n.expr_prefix,
635
+ b: 1,
636
+ r: 'expr',
637
+ g: 'expr,expr-infix,expr-prefix',
638
+ }
639
+ : NONE,
640
+
641
+ // TTT
642
+ hasInfix
643
+ ? {
644
+ s: [INFIX],
645
+ // Complete prefix first.
646
+ c: (r: Rule) => !!r.n.expr_prefix,
647
+ b: 1,
648
+ // r: 'expr',
649
+ g: 'expr,expr-infix',
650
+ }
651
+ : NONE,
652
+
653
+
654
+ hasSuffix
655
+ ? {
656
+ s: [SUFFIX],
657
+ c: (r: Rule) => !r.n.expr_prefix,
658
+ b: 1,
659
+ r: 'expr',
660
+ g: 'expr,expr-suffix,expr-prefix',
661
+ }
662
+ : NONE,
663
+
664
+ // TTT
665
+ // hasSuffix
666
+ // ? {
667
+ // s: [SUFFIX],
668
+ // c: (r: Rule) => !!r.n.expr_prefix,
669
+ // b: 1,
670
+ // r: 'expr',
671
+ // g: 'expr,expr-suffix',
672
+ // }
673
+ // : NONE,
674
+
675
+ hasParen
676
+ ? {
677
+ s: [CP],
678
+ c: (r: Rule) => !!r.n.expr_paren,
679
+ b: 1,
680
+ }
681
+ : NONE,
682
+
683
+ hasTernary
684
+ ? {
685
+ s: [TERN0],
686
+ c: (r: Rule) => !r.n.expr_prefix,
687
+ b: 1,
688
+ r: 'ternary',
689
+ g: 'expr,expr-ternary',
690
+ }
691
+ : NONE,
692
+
693
+ // Implicit list at the top level.
694
+ {
695
+ s: [CA],
696
+ // c: { d: 0 },
697
+ c: (r: Rule) => r.d <= 0,
698
+ n: { expr: 0 },
699
+ r: 'elem',
700
+ a: (r: Rule) => (r.parent.node = r.node = [r.node]),
701
+ g: 'expr,comma,list,top',
702
+ },
703
+
704
+ // Implicit list at the top level.
705
+ {
706
+ s: [VAL],
707
+ // c: { d: 0 },
708
+ c: (r: Rule) => r.d <= 0,
709
+ n: { expr: 0 },
710
+ b: 1,
711
+ r: 'elem',
712
+ a: (r: Rule) => (r.parent.node = r.node = [r.node]),
713
+ g: 'expr,space,list,top',
714
+ },
715
+
716
+ // Implicit list indicated by comma.
717
+ {
718
+ s: [CA],
719
+ c: (r: Rule) => r.lte('pk'),
720
+ n: { expr: 0 },
721
+ b: 1,
722
+ h: implicitList,
723
+ g: 'expr,list,val,imp,comma',
724
+ },
725
+
726
+ // Implicit list indicated by space separated value.
727
+ {
728
+ c: (r: Rule) => r.lte('pk') && r.lte('expr_suffix'),
729
+ n: { expr: 0 },
730
+ h: implicitList,
731
+ g: 'expr,list,val,imp,space',
732
+ },
733
+
734
+ // Expression ends on non-expression token.
735
+ {
736
+ n: { expr: 0 },
737
+ g: 'expr,expr-end',
738
+ },
739
+ ])
740
+
741
+ .ac((r: Rule, ctx: Context) => {
742
+ // Only evaluate at root of expr (where r.n.expr === 0)
743
+
744
+ // console.log('EXPR-AC', r.name, r.i, r.n, p(r.node),
745
+ // 'P', r.parent.name, r.parent.i, r.parent.n, p(r.parent.node))
746
+
747
+ if (options.evaluate && 0 === r.n.expr) {
748
+ // The parent node will contain the root of the expr tree.
749
+ let out = evaluation(
750
+ r.parent,
751
+ ctx,
752
+ r.parent.node,
753
+ options.evaluate,
754
+ )
755
+
756
+ r.parent.node = out
757
+
758
+ // Also write the evaluated result onto this expr rule's own
759
+ // node. When the expr rule was PUSHED from a still-open val
760
+ // (the prefix/paren forms: `+1`, `(a)`), that val's close
761
+ // coalescing reads its child's node (this expr rule). Under
762
+ // the new builtins val-close coalesces child-container over
763
+ // the seeded node, which would otherwise restore the raw
764
+ // op-array and discard the evaluated value. Writing `out`
765
+ // here makes the coalesced value the evaluated result (a
766
+ // primitive scalar or a real container), not the op-array.
767
+ r.node = out
768
+ }
769
+ })
770
+ })
771
+
772
+ rule('paren', (rs: RuleSpec) => {
773
+ rs.bo((r: Rule) => {
774
+ // Allow implicits inside parens
775
+ r.n.dmap = 0
776
+ r.n.dlist = 0
777
+ r.n.pk = 0
778
+ })
779
+ .open([
780
+ hasParen
781
+ ? {
782
+ s: [OP, CP],
783
+ b: 1,
784
+ g: 'expr,expr-paren,empty',
785
+ c: (r: Rule) =>
786
+ parenOTM[r.o0.tin].name === parenCTM[r.o1.tin].name,
787
+ a: makeOpenParen(parenOTM),
788
+ }
789
+ : NONE,
790
+
791
+ hasParen
792
+ ? {
793
+ s: [OP],
794
+ p: 'val',
795
+ n: {
796
+ expr_paren: 1,
797
+ expr: 0,
798
+ expr_prefix: 0,
799
+ expr_suffix: 0,
800
+ },
801
+ g: 'expr,expr-paren,open',
802
+ a: makeOpenParen(parenOTM),
803
+ }
804
+ : NONE,
805
+ ])
806
+
807
+ .close([
808
+ hasParen
809
+ ? {
810
+ s: [CP],
811
+ c: (r: Rule) => {
812
+ const pdef = parenCTM[r.c0.tin]
813
+ let pd = 'expr_paren_depth_' + pdef.name
814
+ return !!r.n[pd]
815
+ },
816
+
817
+ a: makeCloseParen(parenCTM),
818
+ g: 'expr,expr-paren,close',
819
+ }
820
+ : NONE,
821
+ ])
822
+
823
+ .ac((r: Rule, _ctx: Context) => {
824
+ r.parent.node = r.node
825
+ r.parent.parent.node = r.node
826
+ })
827
+ })
828
+
829
+ // Ternary operators are like fancy parens.
830
+ if (hasTernary) {
831
+ rule('ternary', (rs: RuleSpec) => {
832
+ rs.open([
833
+ {
834
+ s: [TERN0],
835
+ p: 'val',
836
+ n: {
837
+ expr_ternary: 1,
838
+ expr: 0,
839
+ expr_prefix: 0,
840
+ expr_suffix: 0,
841
+ },
842
+ u: { expr_ternary_step: 1 },
843
+ g: 'expr,expr-ternary,open',
844
+ a: (r: Rule) => {
845
+ let op = makeOp(r.o0, ternaryTM)
846
+ r.u.expr_ternary_name = op.name
847
+
848
+ if (isOp(r.prev.node)) {
849
+ r.node = updateExprNode(r.prev.node, op, dupNode(r.prev.node))
850
+ } else {
851
+ r.node = r.prev.node = updateExprNode([], op, r.prev.node)
852
+ }
853
+
854
+ r.u.expr_ternary_paren =
855
+ r.n.expr_paren || r.prev.u.expr_ternary_paren || 0
856
+
857
+ r.n.expr_paren = 0
858
+ },
859
+ },
860
+ {
861
+ p: 'val',
862
+ c: (r: Rule) => 2 === r.prev.u.expr_ternary_step,
863
+ a: (r: Rule) => {
864
+ r.u.expr_ternary_step = r.prev.u.expr_ternary_step
865
+ r.n.expr_paren = r.u.expr_ternary_paren =
866
+ r.prev.u.expr_ternary_paren
867
+ },
868
+ g: 'expr,expr-ternary,step',
869
+ },
870
+ ]).close([
871
+ {
872
+ s: [TERN1],
873
+ c: (r: Rule) => {
874
+ return (
875
+ 1 === r.u.expr_ternary_step &&
876
+ r.u.expr_ternary_name === ternaryTM[r.c0.tin].name
877
+ )
878
+ },
879
+ r: 'ternary',
880
+ a: (r: Rule) => {
881
+ r.u.expr_ternary_step++
882
+ r.node.push(r.child.node)
883
+ },
884
+ g: 'expr,expr-ternary,step',
885
+ },
886
+
887
+ // End of ternary at top level. Implicit list indicated by comma.
888
+ {
889
+ s: [[CA, ...CP]],
890
+ c: implicitTernaryCond,
891
+ // Handle ternary as first item of imp list inside paren.
892
+ b: (_r: Rule, ctx: Context) => (CP.includes(ctx.t0.tin) ? 1 : 0),
893
+ r: (r: Rule, ctx: Context) =>
894
+ !CP.includes(ctx.t0.tin) &&
895
+ (0 === r.d ||
896
+ (r.prev.u.expr_ternary_paren && !r.parent.node?.length))
897
+ ? 'elem'
898
+ : '',
899
+ a: implicitTernaryAction,
900
+ g: 'expr,expr-ternary,list,val,imp,comma',
901
+ },
902
+
903
+ // End of ternary at top level.
904
+ // Implicit list indicated by space separated value.
905
+ {
906
+ c: implicitTernaryCond,
907
+ // Handle ternary as first item of imp list inside paren.
908
+ r: (r: Rule, ctx: Context) => {
909
+ return (0 === r.d ||
910
+ !CP.includes(ctx.t0.tin) ||
911
+ r.prev.u.expr_ternary_paren) &&
912
+ !r.parent.node?.length &&
913
+ ZZ !== ctx.t0.tin
914
+ ? 'elem'
915
+ : ''
916
+ },
917
+ a: implicitTernaryAction,
918
+ g: 'expr,expr-ternary,list,val,imp,space',
919
+ },
920
+
921
+ // End of ternary.
922
+ {
923
+ c: (r: Rule) => 0 < r.d && 2 === r.u.expr_ternary_step,
924
+ a: (r: Rule) => {
925
+ r.node.push(r.child.node)
926
+ },
927
+ g: 'expr,expr-ternary,close',
928
+ },
929
+ ])
930
+ // Ensure ternary results get evaluated. Without this, ternaries
931
+ // that aren't wrapped in expr (e.g. val.close's TERN0 alt does
932
+ // `r: 'ternary'` directly, not via an expr intermediate) leave
933
+ // the result as a raw [op_ternary, ...] op-array. Fire on every
934
+ // ternary instance's after-close, but only act when the chain
935
+ // has reached its final step (the node has accumulated all 3
936
+ // operands and r.next is not another ternary). Walk the
937
+ // r.prev chain back to the original rule (the val that started
938
+ // `r: 'ternary'`) and write the evaluated CST so jsonic returns
939
+ // the structured conditional_expression instead of the op-array.
940
+ .ac((r: Rule, ctx: Context) => {
941
+ if (!options.evaluate) return
942
+ // Skip while the chain is still ongoing: r:'ternary' replaces
943
+ // the current rule with another ternary instance, and we want
944
+ // to evaluate only on the FINAL step. r.next is the rule the
945
+ // parser will process after this .ac returns.
946
+ if (r.next && r.next.name === 'ternary') return
947
+ if (!Array.isArray(r.node)) return
948
+ if (!isOp(r.node)) return
949
+ // Op-array isn't fully populated until 3 operands (cond, then,
950
+ // else) sit at r.node[1..3]. Early steps have length 2 or 3.
951
+ if (r.node.length < 4) return
952
+ const out = evaluation(r, ctx, r.node, options.evaluate)
953
+ // Write the evaluated CST back to every rule along the
954
+ // r:-replacement chain (each successive ternary instance and
955
+ // the original val that started the chain). Their .node
956
+ // references currently all point at the same op-array;
957
+ // replace them all with the structured CST so ctx.root().node
958
+ // — whichever one jsonic returns — reflects the evaluated form.
959
+ let cur: any = r
960
+ while (cur) {
961
+ cur.node = out
962
+ cur = cur.prev
963
+ }
964
+ if (r.parent) r.parent.node = out
965
+ })
966
+ })
967
+ }
968
+ }
969
+
970
+ // Convert prior (parent or previous) rule node into an expression.
971
+ function prior(rule: Rule, prior: Rule, op: Op, whence: string) {
972
+ // console.log('PRIOR', whence, rule.i, p(rule.node), 'PR', prior.i, p(prior.node))
973
+
974
+ let prior_node = prior.node
975
+ if (isOp(prior.node) && !op.prefix) {
976
+ // Infix/suffix: the prior node is the genuine first term to wrap
977
+ // (e.g. the `(1+2)` of `(1+2)+3`). Duplicate it so the in-place
978
+ // rewrite below preserves referential integrity.
979
+ prior_node = dupNode(prior.node)
980
+ } else {
981
+ // A prefix op never has a prior term, so any op-array sitting on
982
+ // prior.node here is a foreign parent-seed (a ternary/paren wrapper
983
+ // threaded down by the engine). Start fresh rather than mutating it.
984
+ prior.node = []
985
+ }
986
+
987
+ updateExprNode(prior.node, op)
988
+
989
+ if (!op.prefix) {
990
+ prior.node[1] = prior_node
991
+ }
992
+
993
+ // Ensure first term val rule contains final expression.
994
+ rule.parent = prior
995
+
996
+ return prior.node
997
+ }
998
+
999
+
1000
+ // Add token so that expression evaluator can reference source locations.
1001
+ function makeOp(t: Token, om: OpMap): Op {
1002
+ return { ...om[t.tin], token: t, OP_MARK }
1003
+ }
1004
+
1005
+
1006
+ function updateExprNode(node: any, op: Op, ...terms: any): any {
1007
+ let out = node
1008
+ out[0] = op
1009
+
1010
+ let tI = 0
1011
+ for (; tI < terms.length; tI++) {
1012
+ out[tI + 1] = terms[tI]
1013
+ }
1014
+ out.length = tI + 1
1015
+
1016
+ return out
1017
+ }
1018
+
1019
+ function dupNode(node: any): any {
1020
+ let out: any = [...node]
1021
+ return out
1022
+ }
1023
+
1024
+ function makeOpenParen(parenOTM: OpMap) {
1025
+ return function openParen(r: Rule) {
1026
+ const op = makeOp(r.o0, parenOTM)
1027
+ let pd = 'expr_paren_depth_' + op.name
1028
+ r.u[pd] = r.n[pd] = 1
1029
+ r.node = undefined
1030
+ }
1031
+ }
1032
+
1033
+ function makeCloseParen(parenCTM: OpMap) {
1034
+ return function closeParen(r: Rule) {
1035
+ if (isOp(r.child.node)) {
1036
+ r.node = r.child.node
1037
+ } else if (undefined === r.node) {
1038
+ r.node = r.child.node
1039
+ }
1040
+
1041
+ const op = makeOp(r.c0, parenCTM)
1042
+ let pd = 'expr_paren_depth_' + op.name
1043
+
1044
+ // Construct completed paren expression.
1045
+ if (r.u[pd] === r.n[pd]) {
1046
+
1047
+ const val = r.node
1048
+
1049
+ // r.node = [op.osrc]
1050
+ r.node = [op]
1051
+
1052
+ // WWW
1053
+ if (r.parent.parent?.u?.paren_preval
1054
+ && undefined !== r.parent.parent.node) {
1055
+ r.node.push(r.parent.parent.node)
1056
+ }
1057
+
1058
+ if (undefined !== val) {
1059
+ r.node.push(val)
1060
+ }
1061
+
1062
+ // console.log('MCP', r.i, r.name, p(r.node),
1063
+ // 'PP', r.parent.parent?.u, p(r.parent.parent?.node), 'N', p(r.node))
1064
+
1065
+ // WWW
1066
+ // if (r.parent.prev.u.paren_preval) {
1067
+ // if (isParenOp(r.parent.prev.node)) {
1068
+ // r.node = updateExprNode(
1069
+ // r.parent.prev.node,
1070
+ // r.node[0],
1071
+ // dupNode(r.parent.prev.node),
1072
+ // r.node[1],
1073
+ // )
1074
+ // } else {
1075
+ // r.node.splice(1, 0, r.parent.prev.node)
1076
+ // r.parent.prev.node = r.node
1077
+ // }
1078
+ // }
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+
1084
+ function implicitList(rule: Rule, ctx: Context, a: any) {
1085
+ let paren: Rule | null = null
1086
+
1087
+ // Find the paren rule that contains this implicit list.
1088
+ // If a map or list rule sits between the expression and the paren,
1089
+ // the expression is inside a contained value — not a direct paren
1090
+ // child — so don't create an implicit list.
1091
+ for (let rI = ctx.rsI - 1; -1 < rI; rI--) {
1092
+ if ('paren' === ctx.rs[rI].name) {
1093
+ paren = ctx.rs[rI]
1094
+ break
1095
+ }
1096
+ if ('map' === ctx.rs[rI].name || 'list' === ctx.rs[rI].name) {
1097
+ return a
1098
+ }
1099
+ }
1100
+
1101
+ if (paren) {
1102
+ // Create a list value for the paren rule.
1103
+ if (null == paren.child.node) {
1104
+ paren.child.node = [rule.node]
1105
+ a.r = 'elem'
1106
+ a.b = 0
1107
+ }
1108
+
1109
+ // Convert paren value into a list value.
1110
+ else if (isOp(paren.child.node)) {
1111
+ paren.child.node = [paren.child.node]
1112
+ a.r = 'elem'
1113
+ a.b = 0
1114
+ }
1115
+
1116
+ rule.node = paren.child.node
1117
+ }
1118
+
1119
+ return a
1120
+ }
1121
+
1122
+
1123
+ function implicitTernaryCond(r: Rule) {
1124
+ let cond =
1125
+ (0 === r.d || 1 <= r.n.expr_paren) && !r.n.pk && 2 === r.u.expr_ternary_step
1126
+ return cond
1127
+ }
1128
+
1129
+ function implicitTernaryAction(r: Rule, _ctx: Context, a: AltMatch) {
1130
+ r.n.expr_paren = r.prev.u.expr_ternary_paren
1131
+ r.node.push(r.child.node)
1132
+
1133
+ if ('elem' === a.r) {
1134
+ r.node[0] = dupNode(r.node)
1135
+ r.node.length = 1
1136
+ }
1137
+ }
1138
+
1139
+ function isParenOp(node: any) {
1140
+ return isOpKind('paren', node)
1141
+ }
1142
+
1143
+ function isTernaryOp(node: any) {
1144
+ return isOpKind('ternary', node)
1145
+ }
1146
+
1147
+ function isOpKind(kind: string, node: any) {
1148
+ return null == node ? false : isOp(node) && true === node[0][kind]
1149
+ }
1150
+
1151
+ function isOp(node: any) {
1152
+ return null == node ? false : node[0] && node[0].OP_MARK === OP_MARK
1153
+ }
1154
+
1155
+ function makeOpMap(
1156
+ token: (tkn: string | Tin) => Tin | string,
1157
+ fixed: (tkn: string) => Tin,
1158
+ op: { [name: string]: OpDef },
1159
+ anyfix: 'prefix' | 'suffix' | 'infix' | 'ternary',
1160
+ ): OpMap {
1161
+ return Object.entries(op)
1162
+ .filter(([_, opdef]: [string, OpDef]) => opdef[anyfix])
1163
+ .reduce((odm: OpMap, [name, opdef]: [string, OpDef]) => {
1164
+ let tkn = ''
1165
+ let tin: Tin = -1 as Tin
1166
+ let src = ''
1167
+
1168
+ if ('string' === typeof opdef.src) {
1169
+ src = opdef.src
1170
+ } else {
1171
+ src = (opdef.src as string[])[0]
1172
+ }
1173
+
1174
+ tin = (fixed(src) || token('#E' + src)) as Tin
1175
+ tkn = token(tin) as string
1176
+
1177
+ let op = (odm[tin] = {
1178
+ src: src,
1179
+ left: opdef.left || Number.MIN_SAFE_INTEGER,
1180
+ right: opdef.right || Number.MAX_SAFE_INTEGER,
1181
+ name: name + (name.endsWith('-' + anyfix) ? '' : '-' + anyfix),
1182
+ infix: 'infix' === anyfix,
1183
+ prefix: 'prefix' === anyfix,
1184
+ suffix: 'suffix' === anyfix,
1185
+ ternary: 'ternary' === anyfix,
1186
+ tkn,
1187
+ tin,
1188
+ terms: 'ternary' === anyfix ? 3 : 'infix' === anyfix ? 2 : 1,
1189
+ use: {} as any,
1190
+ paren: false,
1191
+ osrc: '',
1192
+ csrc: '',
1193
+ otkn: '',
1194
+ ctkn: '',
1195
+ otin: -1,
1196
+ ctin: -1,
1197
+ preval: {
1198
+ active: false,
1199
+ required: false,
1200
+ },
1201
+ token: {} as Token,
1202
+ OP_MARK,
1203
+ })
1204
+
1205
+ // Handle the second operator if ternary.
1206
+ if (op.ternary) {
1207
+ let srcs = opdef.src as string[]
1208
+ op.src = srcs[0]
1209
+ op.use.ternary = { opI: 0 }
1210
+
1211
+ let op2 = { ...op }
1212
+ src = (opdef.src as string[])[1]
1213
+
1214
+ tin = (fixed(src) || token('#E' + src)) as Tin
1215
+ tkn = token(tin) as string
1216
+
1217
+ op2.src = src
1218
+ op2.use = { ternary: { opI: 1 } }
1219
+ op2.tkn = tkn
1220
+ op2.tin = tin
1221
+
1222
+ odm[tin] = op2
1223
+ }
1224
+
1225
+ return odm
1226
+ }, {})
1227
+ }
1228
+
1229
+ function makeParenMap(
1230
+ token: (tkn_tin: string | Tin) => Tin | string,
1231
+ fixed: (tkn: string) => Tin,
1232
+ optop: { [name: string]: OpDef },
1233
+ ): OpMap {
1234
+ return entries(optop).reduce((a: OpMap, [name, pdef]: [string, any]) => {
1235
+ if (pdef.paren) {
1236
+ let otin = (fixed(pdef.osrc) || token('#E' + pdef.osrc)) as Tin
1237
+ let otkn = token(otin) as string
1238
+ let ctin = (fixed(pdef.csrc) || token('#E' + pdef.csrc)) as Tin
1239
+ let ctkn = token(ctin) as string
1240
+
1241
+ a[otin] = {
1242
+ name: name + '-paren',
1243
+ osrc: pdef.osrc,
1244
+ csrc: pdef.csrc,
1245
+ otkn,
1246
+ otin,
1247
+ ctkn,
1248
+ ctin,
1249
+ preval: {
1250
+ // True by default if preval specified.
1251
+ active:
1252
+ null == pdef.preval
1253
+ ? false
1254
+ : null == pdef.preval.active
1255
+ ? true
1256
+ : pdef.preval.active,
1257
+ // False by default.
1258
+ required:
1259
+ null == pdef.preval
1260
+ ? false
1261
+ : null == pdef.preval.required
1262
+ ? false
1263
+ : pdef.preval.required,
1264
+ },
1265
+ use: {} as any,
1266
+ paren: true,
1267
+ src: pdef.osrc,
1268
+ // left: -1,
1269
+ // right: -1,
1270
+ left: Number.MIN_SAFE_INTEGER,
1271
+ right: Number.MAX_SAFE_INTEGER,
1272
+ infix: false,
1273
+ prefix: false,
1274
+ suffix: false,
1275
+ ternary: false,
1276
+ tkn: '',
1277
+ tin: -1,
1278
+ terms: 1,
1279
+ token: {} as Token,
1280
+ OP_MARK,
1281
+ }
1282
+ }
1283
+ return a
1284
+ }, {}) as OpMap
1285
+ }
1286
+
1287
+ // Default operator precedence — the "binding power" scale.
1288
+ //
1289
+ // Binding powers are compared only by ORDER, never by magnitude (every
1290
+ // comparison in prattify() is a `<` / `<=`): a higher number binds tighter.
1291
+ // Associativity is set by left vs right — left < right is left-associative
1292
+ // (a-b-c => (a-b)-c); left > right is right-associative. An unset left/right
1293
+ // falls back to MIN/MAX_SAFE_INTEGER (binds loosest on the left, tightest on
1294
+ // the right), which is why prefix ops give only `right` and parens give
1295
+ // neither.
1296
+ //
1297
+ // The defaults occupy a compact low block on a base-1000000 ladder, leaving the
1298
+ // entire range above the built-ins open for client operators. Tiers are 1000000
1299
+ // apart; the +100000 on `right` is the left-associativity offset, so a tier
1300
+ // occupies a 100000-wide band inside its 1000000-wide slot and adjacent tiers
1301
+ // can never overlap. The unary prefix is the tightest built-in (4000000);
1302
+ // everything above it is free for tighter client operators (exponent, postfix,
1303
+ // call/member, …), and looser operators (assignment, ternary, logical, …) slot
1304
+ // in below addition:
1305
+ //
1306
+ // <1000000 looser client ops (assignment, ternary, logical, comparison, …)
1307
+ // 1000000 sequence / comma
1308
+ // 2000000 addition / subtraction <-- built-in
1309
+ // 3000000 multiplication / division / remainder <-- built-in
1310
+ // 4000000 unary prefix (+ -) <-- built-in (tightest)
1311
+ // 5000000 exponent (** ^, right-assoc: left 5100000, right 5000000)
1312
+ // 6000000 postfix / suffix (! ? ++)
1313
+ // 7000000 call / index / member (f() a[i] a.b)
1314
+ // 8000000+ free — the whole range above 4000000 is open for client ops
1315
+ //
1316
+ // To add an operator, pick a tier base = N*1000000 (left = base, right = base +
1317
+ // 100000 for left-assoc; swap them for right-assoc). Tighter than the built-ins?
1318
+ // Use 5000000 and up. Looser? Use below 2000000. Sub-divide any gap with
1319
+ // base + 200000, base + 400000, … — each 1000000-wide gap holds ~4 sub-tiers.
1320
+ Expr.defaults = {
1321
+ op: {
1322
+ positive: {
1323
+ prefix: true,
1324
+ right: 4000000,
1325
+ src: '+',
1326
+ },
1327
+ negative: {
1328
+ prefix: true,
1329
+ right: 4000000,
1330
+ src: '-',
1331
+ },
1332
+
1333
+ // NOTE: all these are left-associative as left < right
1334
+ // Example: 2+3+4 === (2+3)+4
1335
+ addition: {
1336
+ infix: true,
1337
+ left: 2000000,
1338
+ right: 2100000,
1339
+ src: '+',
1340
+ },
1341
+ subtraction: {
1342
+ infix: true,
1343
+ left: 2000000,
1344
+ right: 2100000,
1345
+ src: '-',
1346
+ },
1347
+ multiplication: {
1348
+ infix: true,
1349
+ left: 3000000,
1350
+ right: 3100000,
1351
+ src: '*',
1352
+ },
1353
+ division: {
1354
+ infix: true,
1355
+ left: 3000000,
1356
+ right: 3100000,
1357
+ src: '/',
1358
+ },
1359
+ remainder: {
1360
+ infix: true,
1361
+ left: 3000000,
1362
+ right: 3100000,
1363
+ src: '%',
1364
+ },
1365
+
1366
+ plain: {
1367
+ paren: true,
1368
+ osrc: '(',
1369
+ csrc: ')',
1370
+ },
1371
+ },
1372
+ } as ExprOptions
1373
+
1374
+
1375
+ // Pratt algorithm embeds next operator.
1376
+ // NOTE: preserves referential integrity of root expression.
1377
+ function prattify(expr: any, op?: Op, whence?: string): any[] {
1378
+ // console.log('PRATT-START', whence, p(expr), op?.name)
1379
+
1380
+ let out = expr
1381
+ let expr_op = expr[0]
1382
+
1383
+ if (op) {
1384
+ if (op.infix) {
1385
+ // op is lower
1386
+ if (expr_op.suffix || op.left <= expr_op.right) {
1387
+ updateExprNode(expr, op, dupNode(expr))
1388
+ }
1389
+
1390
+ // op is higher
1391
+ else {
1392
+ const end = expr_op.terms
1393
+
1394
+ if (isOp(expr[end]) && expr[end][0].right < op.left) {
1395
+ out = prattify(expr[end], op, 'prattify-infix')
1396
+ // console.log('PRATT-INFIX-H0', end, out)
1397
+ }
1398
+ else {
1399
+ out = expr[end] = updateExprNode([], op, expr[end])
1400
+ // console.log('PRATT-INFIX-H1', end, out)
1401
+ }
1402
+ }
1403
+ }
1404
+ else if (op.prefix) {
1405
+ out = expr[expr_op.terms] = updateExprNode([], op)
1406
+ }
1407
+ else if (op.suffix) {
1408
+ if (!expr_op.suffix && expr_op.right <= op.left) {
1409
+ const end = expr_op.terms
1410
+
1411
+ // NOTE: special case: higher precedence suffix "drills" into
1412
+ // lower precedence prefixes: @@1! => @(@(1!)), not @((@1)!)
1413
+ if (
1414
+ isOp(expr[end]) &&
1415
+ expr[end][0].prefix &&
1416
+ expr[end][0].right < op.left
1417
+ ) {
1418
+ prattify(expr[end], op, 'prattify-suffix')
1419
+ }
1420
+ else {
1421
+ expr[end] = updateExprNode([], op, expr[end])
1422
+ }
1423
+ }
1424
+ else {
1425
+ updateExprNode(expr, op, dupNode(expr))
1426
+ }
1427
+ }
1428
+ }
1429
+
1430
+ // console.log('PRATT-END', whence, p(out), op?.name)
1431
+ return out
1432
+ }
1433
+
1434
+
1435
+ function evaluation(rule: Rule, ctx: Context, expr: any, evaluate: Evaluate) {
1436
+ let out = expr ?? null
1437
+
1438
+ if (null != expr && isOp(expr)) {
1439
+ out = evaluate(
1440
+ rule,
1441
+ ctx,
1442
+ expr[0],
1443
+ expr.slice(1).map((term: any) => evaluation(rule, ctx, term, evaluate)),
1444
+ )
1445
+ }
1446
+
1447
+ // console.log('EXPR-EVAL', expr, '->', out)
1448
+
1449
+ return out
1450
+ }
1451
+
1452
+
1453
+ function p(node: any, seen?: WeakSet<any>): any {
1454
+ seen = seen ?? new WeakSet<any>()
1455
+ if (seen.has(node)) {
1456
+ return '[CIRCLE]'
1457
+ }
1458
+ else if (null != node && 'object' === typeof node) {
1459
+ seen.add(node)
1460
+ }
1461
+ if (Array.isArray(node) && 'string' === typeof node[0]?.src) {
1462
+ return ['OP' + node[0]?.src, ...(node.slice(1).map((n: any) => p(n, seen)))]
1463
+ }
1464
+ return node
1465
+ }
1466
+
1467
+
1468
+ const testing = {
1469
+ prattify,
1470
+ opify: (x: any) => ((x.OP_MARK = OP_MARK), x),
1471
+ }
1472
+
1473
+ export { Expr, evaluation, testing }
1474
+
1475
+ export type { ExprOptions, OpDef, Op, Evaluate }