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