@tabnas/expr 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/expr.d.ts +91 -7
- package/dist/expr.js +641 -7
- package/dist/expr.js.map +1 -1
- package/dist/translate.js +2 -1
- package/dist/translate.js.map +1 -1
- package/package.json +1 -1
- package/src/expr.ts +823 -13
- package/src/translate.ts +2 -1
package/dist/expr.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* Copyright (c) 2021-2025 Richard Rodger, MIT License */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.translate = exports.testing = exports.Expr = exports.VERSION = void 0;
|
|
4
|
+
exports.translate = exports.testing = exports.FoldError = exports.Expr = exports.VERSION = void 0;
|
|
5
5
|
exports.evaluation = evaluation;
|
|
6
6
|
exports.simplify = simplify;
|
|
7
7
|
exports.parseSimplified = parseSimplified;
|
|
8
|
+
exports.fold = fold;
|
|
9
|
+
exports.recordPositions = recordPositions;
|
|
10
|
+
exports.opify = opify;
|
|
11
|
+
exports.isOp = isOp;
|
|
12
|
+
exports.keyOpOf = keyOpOf;
|
|
13
|
+
exports.makeKeyMap = makeKeyMap;
|
|
8
14
|
// This algorithm is based on Pratt parsing, and draws heavily from
|
|
9
15
|
// the explanation written by Aleksey Kladov here:
|
|
10
16
|
// https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
|
|
@@ -69,6 +75,7 @@ let Expr = function Expr(tn, options) {
|
|
|
69
75
|
const suffixTM = makeOpMap(token, fixed, optop, 'suffix');
|
|
70
76
|
const infixTM = makeOpMap(token, fixed, optop, 'infix');
|
|
71
77
|
const ternaryTM = makeOpMap(token, fixed, optop, 'ternary');
|
|
78
|
+
const keyTM = makeKeyMap(optop);
|
|
72
79
|
const parenOTM = makeParenMap(token, fixed, optop);
|
|
73
80
|
const parenCTM = omap(parenOTM, ([_, pdef]) => [
|
|
74
81
|
undefined,
|
|
@@ -94,6 +101,45 @@ let Expr = function Expr(tn, options) {
|
|
|
94
101
|
},
|
|
95
102
|
},
|
|
96
103
|
});
|
|
104
|
+
// Deferred mode: fold the finished root on the last pass of the parse.
|
|
105
|
+
// The engine's post-process rule event fires after every pass in every
|
|
106
|
+
// grammar; the last pass is a depth-0 close whose alternate neither
|
|
107
|
+
// pushes, replaces nor failed (a rule with no close alternates reports
|
|
108
|
+
// none), outside any probe and not synthesised by recovery. A parse
|
|
109
|
+
// that fails is therefore never folded: the engine reports the failing
|
|
110
|
+
// pass with `alt.err` set, from a handler that swallows subscriber
|
|
111
|
+
// errors. The start rule's own after-close is not that event: a grammar
|
|
112
|
+
// may replace its start rule (yaml's `stream` does on a directive or a
|
|
113
|
+
// document marker), and `parse` returns the original root rule's node,
|
|
114
|
+
// which is what is written here.
|
|
115
|
+
if (options.fold) {
|
|
116
|
+
const foldOptions = options.fold;
|
|
117
|
+
const deferred = {
|
|
118
|
+
...foldOptions,
|
|
119
|
+
op: optop,
|
|
120
|
+
positions: true === foldOptions.positions ? recordPositions(tn) : foldOptions.positions,
|
|
121
|
+
};
|
|
122
|
+
tn.sub({
|
|
123
|
+
ruleDone: (r, ctx, done) => {
|
|
124
|
+
if (0 !== r.d || 'c' !== done.state || done.probe || done.forced)
|
|
125
|
+
return;
|
|
126
|
+
const alt = done.alt;
|
|
127
|
+
if (null != alt && (alt.p || alt.r || alt.err))
|
|
128
|
+
return;
|
|
129
|
+
// A token the last close backtracked over is still in the
|
|
130
|
+
// lookahead buffer, and the engine's trailing-content check fails
|
|
131
|
+
// the parse on it once the rule loop ends: such a parse is not
|
|
132
|
+
// folded, so a fold error cannot stand in for the syntax error.
|
|
133
|
+
// Under recovery the parse returns its value all the same.
|
|
134
|
+
const t0 = ctx.t0;
|
|
135
|
+
if (!ctx.cfg.parse.recover.enabled && null != t0 &&
|
|
136
|
+
ctx.NOTOKEN.tin !== t0.tin && ZZ !== t0.tin)
|
|
137
|
+
return;
|
|
138
|
+
const root = ctx.root();
|
|
139
|
+
root.node = foldTree(root.node, deferred, keyTM);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
97
143
|
// Append 'expr' to the g (group tag) of every alt added below. Mirrors
|
|
98
144
|
// the jsonic grammar(...) setting {rule:{alt:{g:'expr'}}}, applied
|
|
99
145
|
// manually because the plugin uses jsonic.rule() (not jsonic.grammar()).
|
|
@@ -1116,6 +1162,8 @@ function makeOpMap(token, fixed, op, anyfix) {
|
|
|
1116
1162
|
active: false,
|
|
1117
1163
|
required: false,
|
|
1118
1164
|
},
|
|
1165
|
+
key: false,
|
|
1166
|
+
dynamic: false,
|
|
1119
1167
|
token: {},
|
|
1120
1168
|
OP_MARK,
|
|
1121
1169
|
});
|
|
@@ -1180,6 +1228,8 @@ function makeParenMap(token, fixed, optop) {
|
|
|
1180
1228
|
tkn: '',
|
|
1181
1229
|
tin: -1,
|
|
1182
1230
|
terms: 1,
|
|
1231
|
+
key: false,
|
|
1232
|
+
dynamic: false,
|
|
1183
1233
|
token: {},
|
|
1184
1234
|
OP_MARK,
|
|
1185
1235
|
};
|
|
@@ -1187,6 +1237,568 @@ function makeParenMap(token, fixed, optop) {
|
|
|
1187
1237
|
return a;
|
|
1188
1238
|
}, {});
|
|
1189
1239
|
}
|
|
1240
|
+
// Key operators, by the member name that marks a site. A key operator
|
|
1241
|
+
// carries no token, no tin and no binding power; `terms` is 2 (the
|
|
1242
|
+
// member's value, then the sibling map). The table is a Map in the order
|
|
1243
|
+
// a map that owns two of the members is resolved by: static operators
|
|
1244
|
+
// before dynamic ones, then member names in code-point order. Neither
|
|
1245
|
+
// declaration order nor object key order is used: a Go option table has
|
|
1246
|
+
// no declaration order, and a JavaScript object lists integer-like names
|
|
1247
|
+
// first. Two operators on one member name are refused.
|
|
1248
|
+
function makeKeyMap(optop) {
|
|
1249
|
+
const found = [];
|
|
1250
|
+
const owner = new Map();
|
|
1251
|
+
for (const [name, kdef] of entries(optop)) {
|
|
1252
|
+
if (!kdef || !kdef.key)
|
|
1253
|
+
continue;
|
|
1254
|
+
const src = 'string' === typeof kdef.src ? kdef.src : (kdef.src || [])[0];
|
|
1255
|
+
if ('string' !== typeof src || '' === src) {
|
|
1256
|
+
throw new FoldError('fold_key', 'key operator ' + shown(name) + ' names no member');
|
|
1257
|
+
}
|
|
1258
|
+
const prior = owner.get(src);
|
|
1259
|
+
if (undefined !== prior) {
|
|
1260
|
+
throw new FoldError('fold_key', 'key operators ' + shown(prior) + ' and ' + shown(name) + ' both name the member ' + shown(src));
|
|
1261
|
+
}
|
|
1262
|
+
owner.set(src, name);
|
|
1263
|
+
found.push({
|
|
1264
|
+
name: name + (name.endsWith('-key') ? '' : '-key'),
|
|
1265
|
+
src,
|
|
1266
|
+
left: Number.MIN_SAFE_INTEGER,
|
|
1267
|
+
right: Number.MAX_SAFE_INTEGER,
|
|
1268
|
+
use: kdef.use || {},
|
|
1269
|
+
prefix: false,
|
|
1270
|
+
suffix: false,
|
|
1271
|
+
infix: false,
|
|
1272
|
+
ternary: false,
|
|
1273
|
+
paren: false,
|
|
1274
|
+
terms: 2,
|
|
1275
|
+
tkn: '',
|
|
1276
|
+
tin: -1,
|
|
1277
|
+
osrc: '',
|
|
1278
|
+
csrc: '',
|
|
1279
|
+
otkn: '',
|
|
1280
|
+
otin: -1,
|
|
1281
|
+
ctkn: '',
|
|
1282
|
+
ctin: -1,
|
|
1283
|
+
preval: { active: false, required: false },
|
|
1284
|
+
key: true,
|
|
1285
|
+
dynamic: !!kdef.dynamic,
|
|
1286
|
+
token: {},
|
|
1287
|
+
OP_MARK,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
found.sort((a, b) => a.dynamic === b.dynamic ? byCodePoint(a.src, b.src) : a.dynamic ? 1 : -1);
|
|
1291
|
+
return new Map(found.map((op) => [op.src, op]));
|
|
1292
|
+
}
|
|
1293
|
+
// Compare two strings by Unicode code point, which is the order Go and
|
|
1294
|
+
// Rust compare strings in; JavaScript's own `<` compares UTF-16 code
|
|
1295
|
+
// units, which differs above U+FFFF.
|
|
1296
|
+
function byCodePoint(a, b) {
|
|
1297
|
+
const x = Array.from(a);
|
|
1298
|
+
const y = Array.from(b);
|
|
1299
|
+
for (let i = 0; i < x.length && i < y.length; i++) {
|
|
1300
|
+
const d = x[i].codePointAt(0) - y[i].codePointAt(0);
|
|
1301
|
+
if (0 !== d)
|
|
1302
|
+
return d;
|
|
1303
|
+
}
|
|
1304
|
+
return x.length - y.length;
|
|
1305
|
+
}
|
|
1306
|
+
// A plain record: an object whose prototype is `null` (the engine's
|
|
1307
|
+
// maps) or `Object.prototype` (`JSON.parse`, a literal). Anything else, a
|
|
1308
|
+
// Map, a Date, a class instance, a boxed value, is a leaf to a fold and
|
|
1309
|
+
// to `evaluation`: passed through by identity, never rebuilt, since a
|
|
1310
|
+
// rebuilt copy would carry the prototype without the internal state.
|
|
1311
|
+
function isRecord(v) {
|
|
1312
|
+
if (null == v || 'object' !== typeof v || Array.isArray(v))
|
|
1313
|
+
return false;
|
|
1314
|
+
const proto = Object.getPrototypeOf(v);
|
|
1315
|
+
return null === proto || Object.prototype === proto;
|
|
1316
|
+
}
|
|
1317
|
+
// The key operator a map is an occurrence of, by shape: the first, in the
|
|
1318
|
+
// table's order, whose member the map owns. Undefined for anything else.
|
|
1319
|
+
function keyOpOf(keys, node) {
|
|
1320
|
+
if (!isRecord(node))
|
|
1321
|
+
return undefined;
|
|
1322
|
+
for (const op of keys.values()) {
|
|
1323
|
+
if (Object.prototype.hasOwnProperty.call(node, op.src))
|
|
1324
|
+
return op;
|
|
1325
|
+
}
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
// RFC 6901: a pointer from path segments.
|
|
1329
|
+
function pointer(path) {
|
|
1330
|
+
let out = '';
|
|
1331
|
+
for (const seg of path) {
|
|
1332
|
+
out += '/' + String(seg).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1333
|
+
}
|
|
1334
|
+
return out;
|
|
1335
|
+
}
|
|
1336
|
+
// An RFC 6901 array index: `0`, or digits with no leading zero, given as
|
|
1337
|
+
// a string, or a non-negative integer; -1 for anything else, `-` and
|
|
1338
|
+
// out-of-range indices included.
|
|
1339
|
+
const INDEX = /^(0|[1-9][0-9]*)$/;
|
|
1340
|
+
function arrayIndex(seg, length) {
|
|
1341
|
+
const i = 'number' === typeof seg
|
|
1342
|
+
? (Number.isInteger(seg) ? seg : -1)
|
|
1343
|
+
: (INDEX.test(seg) ? Number(seg) : -1);
|
|
1344
|
+
return 0 <= i && i < length ? i : -1;
|
|
1345
|
+
}
|
|
1346
|
+
// One step of a path: an index into a list, or an own member of a plain
|
|
1347
|
+
// record. NOTHING when the step names nothing.
|
|
1348
|
+
const NOTHING = Symbol('nothing');
|
|
1349
|
+
function step(node, seg) {
|
|
1350
|
+
if (Array.isArray(node)) {
|
|
1351
|
+
const i = arrayIndex(seg, node.length);
|
|
1352
|
+
return i < 0 ? NOTHING : node[i];
|
|
1353
|
+
}
|
|
1354
|
+
if (isRecord(node) && Object.prototype.hasOwnProperty.call(node, seg))
|
|
1355
|
+
return node[seg];
|
|
1356
|
+
return NOTHING;
|
|
1357
|
+
}
|
|
1358
|
+
// A new container of the same prototype as `like` (the engine builds
|
|
1359
|
+
// null-prototype objects), with members DEFINED rather than assigned so a
|
|
1360
|
+
// member named `__proto__` stays data.
|
|
1361
|
+
function newRecord(like) {
|
|
1362
|
+
return Object.create(Object.getPrototypeOf(like));
|
|
1363
|
+
}
|
|
1364
|
+
function define(o, k, v) {
|
|
1365
|
+
Object.defineProperty(o, k, { value: v, enumerable: true, writable: true, configurable: true });
|
|
1366
|
+
}
|
|
1367
|
+
// The fold's own errors. `code` is what a fixture pins: `fold_cycle` (a
|
|
1368
|
+
// cycle under `cycle: 'error'`), `fold_depth` (nesting and reference hops
|
|
1369
|
+
// past `maxDepth`, or the stack ran out first), `fold_inherit` (an
|
|
1370
|
+
// `inherit` that returned no bag, or folded its own subtree), `fold_key`
|
|
1371
|
+
// (an operator table with two key operators on one member name, or one
|
|
1372
|
+
// on none) and `fold_option` (`positions: true` outside deferred mode).
|
|
1373
|
+
// The document key and the pointers are fields; the message quotes them
|
|
1374
|
+
// JSON-escaped and cut short, since member names are document content.
|
|
1375
|
+
class FoldError extends Error {
|
|
1376
|
+
constructor(code, message, fields) {
|
|
1377
|
+
super('expr fold: ' + message);
|
|
1378
|
+
this.name = 'FoldError';
|
|
1379
|
+
this.code = code;
|
|
1380
|
+
if (fields)
|
|
1381
|
+
Object.assign(this, fields);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
exports.FoldError = FoldError;
|
|
1385
|
+
// A key or pointer for a message: JSON-escaped, so a newline or an escape
|
|
1386
|
+
// sequence in a member name stays visible, and at most about 80
|
|
1387
|
+
// characters long.
|
|
1388
|
+
function shown(s) {
|
|
1389
|
+
const q = JSON.stringify(s);
|
|
1390
|
+
return q.length <= 80 ? q : q.slice(0, 76) + '..."';
|
|
1391
|
+
}
|
|
1392
|
+
// Record the opening token of every container a parse builds, by the
|
|
1393
|
+
// container's identity, and return the lookup `fold` takes as
|
|
1394
|
+
// `positions`. It subscribes to the engine's post-process rule event on
|
|
1395
|
+
// the instance, so it works in any grammar; the table is weak, so a
|
|
1396
|
+
// parsed tree's entries go with the tree.
|
|
1397
|
+
function recordPositions(tn) {
|
|
1398
|
+
const tokens = new WeakMap();
|
|
1399
|
+
tn.sub({
|
|
1400
|
+
ruleDone: (r, _ctx, done) => {
|
|
1401
|
+
if ('o' !== done.state)
|
|
1402
|
+
return;
|
|
1403
|
+
const node = r.node;
|
|
1404
|
+
if (null != node && 'object' === typeof node && !tokens.has(node) && r.o0) {
|
|
1405
|
+
tokens.set(node, r.o0);
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
});
|
|
1409
|
+
return (node) => tokens.get(node);
|
|
1410
|
+
}
|
|
1411
|
+
class PNode {
|
|
1412
|
+
constructor(doc, parent, seg, raw) {
|
|
1413
|
+
this.doc = doc;
|
|
1414
|
+
this.parent = parent;
|
|
1415
|
+
this.seg = seg;
|
|
1416
|
+
this.raw = raw;
|
|
1417
|
+
this.kids = undefined;
|
|
1418
|
+
// 0 not yet folded, 1 being folded (on the active stack), 2 folded.
|
|
1419
|
+
this.state = 0;
|
|
1420
|
+
this.value = undefined;
|
|
1421
|
+
// How the walk entered it: 0 from its parent, 1 through a reference.
|
|
1422
|
+
this.via = 0;
|
|
1423
|
+
this.k = undefined;
|
|
1424
|
+
this.kBusy = false;
|
|
1425
|
+
// Its resolution met a cycle, so the resolver was told `cycle: true`.
|
|
1426
|
+
this.cut = false;
|
|
1427
|
+
// A member of a loop of references with no container in it, or a
|
|
1428
|
+
// reference into one: there is nothing to resolve it to.
|
|
1429
|
+
this.loop = false;
|
|
1430
|
+
// The key operator this map is an occurrence of, decided once (shape,
|
|
1431
|
+
// then the host's `recognise`): undefined until decided, null for none.
|
|
1432
|
+
this.site = undefined;
|
|
1433
|
+
this.parts = undefined;
|
|
1434
|
+
this.plainDone = false;
|
|
1435
|
+
this.plainValue = undefined;
|
|
1436
|
+
this.ptr = undefined;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// Fold a finished tree: walk it, reduce every key-operator site
|
|
1440
|
+
// bottom-up through `opts.evaluate`, and return a NEW tree; the input is
|
|
1441
|
+
// never changed (though a value a resolver returns, `site.node` among
|
|
1442
|
+
// them, is placed in the output as it is). Values flow up through
|
|
1443
|
+
// `evaluate`; context flows down through `inherit`, computed per path
|
|
1444
|
+
// from each map's own ancestors and kept. One result per location, so
|
|
1445
|
+
// every site naming one target shares one folded value. Every container
|
|
1446
|
+
// being folded is on an active stack: a site whose target is on it
|
|
1447
|
+
// closes a cycle, which the resolver is told about (or which throws).
|
|
1448
|
+
// Dynamic key operators are reported and folded as plain maps. Token
|
|
1449
|
+
// operator arrays met in the walk are reduced through the same
|
|
1450
|
+
// `evaluate` when `tokens` is on.
|
|
1451
|
+
function fold(root, opts) {
|
|
1452
|
+
return foldTree(root, opts, makeKeyMap(opts.op || {}));
|
|
1453
|
+
}
|
|
1454
|
+
function foldTree(root, opts, keys) {
|
|
1455
|
+
if (true === opts.positions) {
|
|
1456
|
+
throw new FoldError('fold_option', 'positions: true records tokens during a parse, so it works in the deferred mode only;'
|
|
1457
|
+
+ ' pass a lookup, such as recordPositions(tn), to fold');
|
|
1458
|
+
}
|
|
1459
|
+
const tokens = false !== opts.tokens;
|
|
1460
|
+
const inherit = opts.inherit;
|
|
1461
|
+
const recognise = opts.recognise;
|
|
1462
|
+
const lookup = 'function' === typeof opts.positions ? opts.positions : undefined;
|
|
1463
|
+
const maxDepth = null == opts.maxDepth ? 1000 : opts.maxDepth;
|
|
1464
|
+
const base = opts.k || {};
|
|
1465
|
+
const report = { cycles: [], unresolved: [], dynamic: [] };
|
|
1466
|
+
const noted = new Set();
|
|
1467
|
+
const stack = [];
|
|
1468
|
+
const docs = new Map();
|
|
1469
|
+
const makeDoc = (key, raw) => {
|
|
1470
|
+
const doc = { key, root: raw };
|
|
1471
|
+
doc.top = new PNode(doc, undefined, '', raw);
|
|
1472
|
+
docs.set(key, doc);
|
|
1473
|
+
return doc;
|
|
1474
|
+
};
|
|
1475
|
+
// Another document, by key, loaded once; undefined when it cannot be had.
|
|
1476
|
+
const docOf = (from, key) => {
|
|
1477
|
+
if (undefined === key || key === from.doc.key)
|
|
1478
|
+
return from.doc;
|
|
1479
|
+
let doc = docs.get(key);
|
|
1480
|
+
if (undefined === doc) {
|
|
1481
|
+
const raw = undefined === opts.load ? undefined : opts.load(key, siteOf(from));
|
|
1482
|
+
doc = undefined === raw ? null : makeDoc(key, raw);
|
|
1483
|
+
docs.set(key, doc);
|
|
1484
|
+
}
|
|
1485
|
+
return null === doc ? undefined : doc;
|
|
1486
|
+
};
|
|
1487
|
+
const kid = (pn, seg, raw) => {
|
|
1488
|
+
const name = String(seg);
|
|
1489
|
+
let kids = pn.kids;
|
|
1490
|
+
if (undefined === kids)
|
|
1491
|
+
kids = pn.kids = new Map();
|
|
1492
|
+
let node = kids.get(name);
|
|
1493
|
+
if (undefined === node) {
|
|
1494
|
+
node = new PNode(pn.doc, pn, seg, raw);
|
|
1495
|
+
kids.set(name, node);
|
|
1496
|
+
}
|
|
1497
|
+
return node;
|
|
1498
|
+
};
|
|
1499
|
+
const ptrOf = (pn) => {
|
|
1500
|
+
if (undefined !== pn.ptr)
|
|
1501
|
+
return pn.ptr;
|
|
1502
|
+
const chain = [];
|
|
1503
|
+
let p = pn;
|
|
1504
|
+
while (undefined !== p && undefined === p.ptr) {
|
|
1505
|
+
chain.push(p);
|
|
1506
|
+
p = p.parent;
|
|
1507
|
+
}
|
|
1508
|
+
let s = undefined === p ? '' : p.ptr;
|
|
1509
|
+
for (let i = chain.length - 1; 0 <= i; i--) {
|
|
1510
|
+
const c = chain[i];
|
|
1511
|
+
s = undefined === c.parent ? '' : s + '/' + String(c.seg).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1512
|
+
c.ptr = s;
|
|
1513
|
+
}
|
|
1514
|
+
return s;
|
|
1515
|
+
};
|
|
1516
|
+
const pathOf = (pn) => {
|
|
1517
|
+
const path = [];
|
|
1518
|
+
for (let p = pn; undefined !== p && undefined !== p.parent; p = p.parent) {
|
|
1519
|
+
path.push(p.seg);
|
|
1520
|
+
}
|
|
1521
|
+
return path.reverse();
|
|
1522
|
+
};
|
|
1523
|
+
const note = (list, row) => {
|
|
1524
|
+
const id = JSON.stringify([list, row.doc, row.at, row.targetDoc, row.target, row.src]);
|
|
1525
|
+
if (noted.has(id))
|
|
1526
|
+
return;
|
|
1527
|
+
noted.add(id);
|
|
1528
|
+
report[list].push(row);
|
|
1529
|
+
};
|
|
1530
|
+
// The bag at a map: the `inherit` calls of its ancestors and of the map
|
|
1531
|
+
// itself, from the root down, each kept, worked out without recursion.
|
|
1532
|
+
const kOf = (pn) => {
|
|
1533
|
+
if (undefined === inherit)
|
|
1534
|
+
return base;
|
|
1535
|
+
if (undefined !== pn.k)
|
|
1536
|
+
return pn.k;
|
|
1537
|
+
const chain = [];
|
|
1538
|
+
let p = pn;
|
|
1539
|
+
while (undefined !== p && undefined === p.k) {
|
|
1540
|
+
if (p.kBusy) {
|
|
1541
|
+
throw new FoldError('fold_inherit', 'inherit folded its own subtree at ' + shown(ptrOf(p)), { doc: p.doc.key, at: ptrOf(p) });
|
|
1542
|
+
}
|
|
1543
|
+
chain.push(p);
|
|
1544
|
+
p = p.parent;
|
|
1545
|
+
}
|
|
1546
|
+
let k = undefined === p ? base : p.k;
|
|
1547
|
+
for (let i = chain.length - 1; 0 <= i; i--) {
|
|
1548
|
+
const c = chain[i];
|
|
1549
|
+
if (isRecord(c.raw)) {
|
|
1550
|
+
c.kBusy = true;
|
|
1551
|
+
let next;
|
|
1552
|
+
try {
|
|
1553
|
+
next = inherit(siteOf(c, k), c.raw, k);
|
|
1554
|
+
}
|
|
1555
|
+
finally {
|
|
1556
|
+
c.kBusy = false;
|
|
1557
|
+
}
|
|
1558
|
+
if (null == next || 'object' !== typeof next) {
|
|
1559
|
+
throw new FoldError('fold_inherit', 'inherit returned no bag at ' + shown(ptrOf(c)), { doc: c.doc.key, at: ptrOf(c) });
|
|
1560
|
+
}
|
|
1561
|
+
k = next;
|
|
1562
|
+
}
|
|
1563
|
+
c.k = k;
|
|
1564
|
+
}
|
|
1565
|
+
return k;
|
|
1566
|
+
};
|
|
1567
|
+
const siteOf = (pn, parentK) => {
|
|
1568
|
+
let path;
|
|
1569
|
+
const node = pn.raw;
|
|
1570
|
+
return {
|
|
1571
|
+
root: pn.doc.root,
|
|
1572
|
+
doc: pn.doc.key,
|
|
1573
|
+
get path() {
|
|
1574
|
+
return undefined === path ? (path = pathOf(pn)) : path;
|
|
1575
|
+
},
|
|
1576
|
+
node,
|
|
1577
|
+
get k() {
|
|
1578
|
+
return undefined === parentK ? kOf(pn) : parentK;
|
|
1579
|
+
},
|
|
1580
|
+
token: undefined !== lookup && null != node && 'object' === typeof node ? lookup(node) : undefined,
|
|
1581
|
+
at: (target, doc) => at(pn, target, doc),
|
|
1582
|
+
locate: (target, doc) => locate(pn, target, doc),
|
|
1583
|
+
plain: () => plainOf(pn),
|
|
1584
|
+
};
|
|
1585
|
+
};
|
|
1586
|
+
const locate = (from, target, key) => {
|
|
1587
|
+
const doc = docOf(from, key);
|
|
1588
|
+
if (undefined === doc)
|
|
1589
|
+
return { found: false };
|
|
1590
|
+
let node = doc.root;
|
|
1591
|
+
for (const seg of target) {
|
|
1592
|
+
node = step(node, seg);
|
|
1593
|
+
if (NOTHING === node)
|
|
1594
|
+
return { found: false };
|
|
1595
|
+
}
|
|
1596
|
+
return { found: true, value: node };
|
|
1597
|
+
};
|
|
1598
|
+
const miss = (from, targetDoc, target) => {
|
|
1599
|
+
note('unresolved', { doc: from.doc.key, at: ptrOf(from), targetDoc, target: pointer(target) });
|
|
1600
|
+
return { found: false, cycle: false };
|
|
1601
|
+
};
|
|
1602
|
+
// A cycle: `from` named `target` while `target` was still being folded
|
|
1603
|
+
// (a back edge), or `target` is in a loop of references. Every member of
|
|
1604
|
+
// a loop made only of references is a cycle, and so is every reference
|
|
1605
|
+
// into one, since none of them has anything to resolve to. A cycle that
|
|
1606
|
+
// passes through a container is cut once, at the site whose target was
|
|
1607
|
+
// being folded; a later reference to that site gets its folded value.
|
|
1608
|
+
const cycleTo = (from, target) => {
|
|
1609
|
+
const row = { doc: from.doc.key, at: ptrOf(from), targetDoc: target.doc.key, target: ptrOf(target) };
|
|
1610
|
+
if ('error' === opts.cycle) {
|
|
1611
|
+
throw new FoldError('fold_cycle', 'cycle at ' + shown(row.at) + ' to ' + shown(row.target), row);
|
|
1612
|
+
}
|
|
1613
|
+
note('cycles', row);
|
|
1614
|
+
from.cut = true;
|
|
1615
|
+
if (1 === target.state) {
|
|
1616
|
+
const i = stack.lastIndexOf(target);
|
|
1617
|
+
let refs = 0 <= i;
|
|
1618
|
+
for (let j = i + 1; refs && j < stack.length; j++)
|
|
1619
|
+
refs = 1 === stack[j].via;
|
|
1620
|
+
if (refs) {
|
|
1621
|
+
for (let j = i; j < stack.length; j++)
|
|
1622
|
+
stack[j].loop = true;
|
|
1623
|
+
from.loop = true;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
else if (target.loop) {
|
|
1627
|
+
from.loop = true;
|
|
1628
|
+
}
|
|
1629
|
+
return { found: true, cycle: true };
|
|
1630
|
+
};
|
|
1631
|
+
// The folded value at `target` in a document, for the site at `from`.
|
|
1632
|
+
// Raw members are followed from the root; a key-operator map met on the
|
|
1633
|
+
// way is folded (through the memo) and the remaining segments read its
|
|
1634
|
+
// folded value, so a reference to a reference resolves. A target still
|
|
1635
|
+
// being folded is a cycle, and so is a path that must pass through a
|
|
1636
|
+
// site that was cut.
|
|
1637
|
+
const at = (from, target, key) => {
|
|
1638
|
+
const targetDoc = undefined === key ? from.doc.key : key;
|
|
1639
|
+
const doc = docOf(from, key);
|
|
1640
|
+
if (undefined === doc)
|
|
1641
|
+
return miss(from, targetDoc, target);
|
|
1642
|
+
let pn = doc.top;
|
|
1643
|
+
let node = doc.root;
|
|
1644
|
+
for (let i = 0; i < target.length; i++) {
|
|
1645
|
+
const v = step(node, target[i]);
|
|
1646
|
+
if (NOTHING === v)
|
|
1647
|
+
return miss(from, targetDoc, target);
|
|
1648
|
+
if (i === target.length - 1 && !Array.isArray(v) && !isRecord(v)) {
|
|
1649
|
+
return { found: true, cycle: false, value: v };
|
|
1650
|
+
}
|
|
1651
|
+
pn = kid(pn, Array.isArray(node) ? arrayIndex(target[i], node.length) : String(target[i]), v);
|
|
1652
|
+
node = v;
|
|
1653
|
+
// Only a static site changes what lies below it; any other map,
|
|
1654
|
+
// a dynamic one or one `recognise` refused, folds member by member,
|
|
1655
|
+
// so the raw path below it names the same location.
|
|
1656
|
+
const op = i < target.length - 1 && isRecord(node) ? siteOpOf(pn) : null;
|
|
1657
|
+
if (null !== op && !op.dynamic) {
|
|
1658
|
+
if (1 === pn.state || pn.loop)
|
|
1659
|
+
return cycleTo(from, pn);
|
|
1660
|
+
let folded = walk(pn, 1);
|
|
1661
|
+
if (pn.loop || pn.cut)
|
|
1662
|
+
return cycleTo(from, pn);
|
|
1663
|
+
for (i++; i < target.length; i++) {
|
|
1664
|
+
folded = step(folded, target[i]);
|
|
1665
|
+
if (NOTHING === folded)
|
|
1666
|
+
return miss(from, targetDoc, target);
|
|
1667
|
+
}
|
|
1668
|
+
return { found: true, cycle: false, value: folded };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (1 === pn.state)
|
|
1672
|
+
return cycleTo(from, pn);
|
|
1673
|
+
const value = walk(pn, 1);
|
|
1674
|
+
if (pn.loop)
|
|
1675
|
+
return cycleTo(from, pn);
|
|
1676
|
+
return { found: true, cycle: false, value };
|
|
1677
|
+
};
|
|
1678
|
+
// The key operator a map is an occurrence of here, if any: its shape,
|
|
1679
|
+
// then the host's `recognise`, decided once per location.
|
|
1680
|
+
const siteOpOf = (pn) => {
|
|
1681
|
+
if (undefined !== pn.site)
|
|
1682
|
+
return pn.site;
|
|
1683
|
+
let op = keyOpOf(keys, pn.raw) ?? null;
|
|
1684
|
+
pn.site = op;
|
|
1685
|
+
if (null !== op && undefined !== recognise && !recognise(siteOf(pn), op))
|
|
1686
|
+
op = pn.site = null;
|
|
1687
|
+
return op;
|
|
1688
|
+
};
|
|
1689
|
+
// A member's folded value: a container through the memo, anything else
|
|
1690
|
+
// as it is.
|
|
1691
|
+
const member = (pn, seg, child) => Array.isArray(child) || isRecord(child) ? walk(kid(pn, seg, child), 0) : child;
|
|
1692
|
+
// A map folded as a plain map, members in its own order.
|
|
1693
|
+
const plainOf = (pn) => {
|
|
1694
|
+
if (pn.plainDone)
|
|
1695
|
+
return pn.plainValue;
|
|
1696
|
+
const node = pn.raw;
|
|
1697
|
+
if (!isRecord(node))
|
|
1698
|
+
return walk(pn, 0);
|
|
1699
|
+
const parts = pn.parts;
|
|
1700
|
+
const out = newRecord(node);
|
|
1701
|
+
for (const name of Object.keys(node)) {
|
|
1702
|
+
define(out, name, undefined === parts ? member(pn, name, node[name])
|
|
1703
|
+
: name === parts.op.src ? parts.value : parts.siblings[name]);
|
|
1704
|
+
}
|
|
1705
|
+
pn.plainDone = true;
|
|
1706
|
+
pn.plainValue = out;
|
|
1707
|
+
return out;
|
|
1708
|
+
};
|
|
1709
|
+
const walk = (pn, via) => {
|
|
1710
|
+
if (2 === pn.state)
|
|
1711
|
+
return pn.value;
|
|
1712
|
+
const node = pn.raw;
|
|
1713
|
+
const list = Array.isArray(node);
|
|
1714
|
+
if (!list && (!isRecord(node) || OP_MARK === node.OP_MARK))
|
|
1715
|
+
return node;
|
|
1716
|
+
if (1 === pn.state)
|
|
1717
|
+
return node;
|
|
1718
|
+
if (maxDepth <= stack.length) {
|
|
1719
|
+
throw new FoldError('fold_depth', 'nesting and references deeper than ' + maxDepth + ' at ' + shown(ptrOf(pn)), { doc: pn.doc.key, at: ptrOf(pn) });
|
|
1720
|
+
}
|
|
1721
|
+
pn.state = 1;
|
|
1722
|
+
pn.via = via;
|
|
1723
|
+
stack.push(pn);
|
|
1724
|
+
let out;
|
|
1725
|
+
try {
|
|
1726
|
+
if (list && isOp(node)) {
|
|
1727
|
+
// Each term is folded at its own path first, so a key-operator
|
|
1728
|
+
// site inside a token expression resolves, and a nested
|
|
1729
|
+
// expression is kept by path. With `tokens` off the operator
|
|
1730
|
+
// node is rebuilt around its own head, so it stays an operator.
|
|
1731
|
+
const terms = [];
|
|
1732
|
+
for (let i = 1; i < node.length; i++)
|
|
1733
|
+
terms.push(member(pn, i, node[i]));
|
|
1734
|
+
out = tokens ? opts.evaluate(siteOf(pn), node[0], terms) : [node[0], ...terms];
|
|
1735
|
+
}
|
|
1736
|
+
else if (list) {
|
|
1737
|
+
// Members are walked inline, not through `member`, so a level of
|
|
1738
|
+
// nesting costs one stack frame.
|
|
1739
|
+
out = [];
|
|
1740
|
+
for (let i = 0; i < node.length; i++) {
|
|
1741
|
+
const c = node[i];
|
|
1742
|
+
out.push(Array.isArray(c) || isRecord(c) ? walk(kid(pn, i, c), 0) : c);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
const op = siteOpOf(pn);
|
|
1747
|
+
if (null === op || op.dynamic) {
|
|
1748
|
+
if (null !== op)
|
|
1749
|
+
note('dynamic', { doc: pn.doc.key, at: ptrOf(pn), src: op.src });
|
|
1750
|
+
if (pn.plainDone) {
|
|
1751
|
+
out = pn.plainValue;
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
out = newRecord(node);
|
|
1755
|
+
for (const name of Object.keys(node)) {
|
|
1756
|
+
const c = node[name];
|
|
1757
|
+
define(out, name, Array.isArray(c) || isRecord(c) ? walk(kid(pn, name, c), 0) : c);
|
|
1758
|
+
}
|
|
1759
|
+
pn.plainDone = true;
|
|
1760
|
+
pn.plainValue = out;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
const siblings = newRecord(node);
|
|
1765
|
+
let value;
|
|
1766
|
+
for (const name of Object.keys(node)) {
|
|
1767
|
+
const v = member(pn, name, node[name]);
|
|
1768
|
+
if (name === op.src)
|
|
1769
|
+
value = v;
|
|
1770
|
+
else
|
|
1771
|
+
define(siblings, name, v);
|
|
1772
|
+
}
|
|
1773
|
+
pn.parts = { op, value, siblings };
|
|
1774
|
+
const site = siteOf(pn);
|
|
1775
|
+
out = opts.evaluate(site, { ...op, token: site.token || op.token }, [value, siblings]);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
finally {
|
|
1780
|
+
stack.pop();
|
|
1781
|
+
pn.state = 0;
|
|
1782
|
+
}
|
|
1783
|
+
pn.state = 2;
|
|
1784
|
+
pn.value = out;
|
|
1785
|
+
return out;
|
|
1786
|
+
};
|
|
1787
|
+
const main = makeDoc(null == opts.doc ? '' : opts.doc, root);
|
|
1788
|
+
let out;
|
|
1789
|
+
try {
|
|
1790
|
+
out = walk(main.top, 0);
|
|
1791
|
+
}
|
|
1792
|
+
catch (err) {
|
|
1793
|
+
if (err instanceof RangeError && /call stack/i.test(String(err.message))) {
|
|
1794
|
+
throw new FoldError('fold_depth', 'the stack ran out before maxDepth (' + maxDepth + ') was reached; lower maxDepth', { doc: main.key });
|
|
1795
|
+
}
|
|
1796
|
+
throw err;
|
|
1797
|
+
}
|
|
1798
|
+
if (opts.report)
|
|
1799
|
+
opts.report(report);
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1190
1802
|
// Default operator precedence — the "binding power" scale.
|
|
1191
1803
|
//
|
|
1192
1804
|
// Binding powers are compared only by ORDER, never by magnitude (every
|
|
@@ -1330,13 +1942,13 @@ function prattify(expr, op, whence) {
|
|
|
1330
1942
|
// member's position in the list. Weak, so an entry lasts only as long as
|
|
1331
1943
|
// the value itself.
|
|
1332
1944
|
const EVALUATED = new WeakSet();
|
|
1333
|
-
function evaluation(rule, ctx, expr, evaluate) {
|
|
1945
|
+
function evaluation(rule, ctx, expr, evaluate, opts) {
|
|
1334
1946
|
let out = expr ?? null;
|
|
1335
1947
|
if (null == expr || EVALUATED.has(expr)) {
|
|
1336
1948
|
return out;
|
|
1337
1949
|
}
|
|
1338
1950
|
if (isOp(expr)) {
|
|
1339
|
-
out = evaluate(rule, ctx, expr[0], expr.slice(1).map((term) => evaluation(rule, ctx, term, evaluate)));
|
|
1951
|
+
out = evaluate(rule, ctx, expr[0], expr.slice(1).map((term) => evaluation(rule, ctx, term, evaluate, opts)));
|
|
1340
1952
|
}
|
|
1341
1953
|
// An implicit list — `f(1+2, 3+4)`, `1+2, 3+4` — is a plain array, not
|
|
1342
1954
|
// an op-array, so the branch above does not reach its members. Reduce
|
|
@@ -1347,9 +1959,22 @@ function evaluation(rule, ctx, expr, evaluate) {
|
|
|
1347
1959
|
// the expr rule's close hook does that itself — mutating its own tree
|
|
1348
1960
|
// is the parser's business, not this function's.
|
|
1349
1961
|
else if (Array.isArray(expr)) {
|
|
1350
|
-
out = expr.map((term) => evaluation(rule, ctx, term, evaluate));
|
|
1962
|
+
out = expr.map((term) => evaluation(rule, ctx, term, evaluate, opts));
|
|
1963
|
+
}
|
|
1964
|
+
// A plain object is passed through unchanged by default, as the
|
|
1965
|
+
// reference documents; with `objects` on it is reduced member by member
|
|
1966
|
+
// into a NEW object of the same prototype, which is what the eager
|
|
1967
|
+
// `evaluate` option already does for an expression inside a map.
|
|
1968
|
+
else if (opts && opts.objects && isRecord(expr)) {
|
|
1969
|
+
const o = newRecord(expr);
|
|
1970
|
+
for (const k of Object.keys(expr))
|
|
1971
|
+
define(o, k, evaluation(rule, ctx, expr[k], evaluate, opts));
|
|
1972
|
+
out = o;
|
|
1351
1973
|
}
|
|
1352
|
-
|
|
1974
|
+
// Only a value this fold PRODUCED is marked: a node passed through
|
|
1975
|
+
// unchanged (a plain object, by default) is not, so a later fold with
|
|
1976
|
+
// `objects` on still reaches the operators inside it.
|
|
1977
|
+
if (null != out && 'object' === typeof out && out !== expr) {
|
|
1353
1978
|
EVALUATED.add(out);
|
|
1354
1979
|
}
|
|
1355
1980
|
// console.log('EXPR-EVAL', expr, '->', out)
|
|
@@ -1412,15 +2037,24 @@ function simplify(value) {
|
|
|
1412
2037
|
function parseSimplified(tn, src, meta) {
|
|
1413
2038
|
return simplify(tn.parse(src, meta));
|
|
1414
2039
|
}
|
|
2040
|
+
// Mark a hand-built operator description as this plugin's, so `isOp` and
|
|
2041
|
+
// `evaluation` recognise a node headed by it. A sibling plugin that builds
|
|
2042
|
+
// operator nodes needs this; `simplify` needs it not, reading any
|
|
2043
|
+
// `src`-headed array. Fills nothing else: pass a complete description.
|
|
2044
|
+
function opify(op) {
|
|
2045
|
+
;
|
|
2046
|
+
op.OP_MARK = OP_MARK;
|
|
2047
|
+
return op;
|
|
2048
|
+
}
|
|
1415
2049
|
const testing = {
|
|
1416
2050
|
prattify,
|
|
1417
|
-
opify
|
|
2051
|
+
opify,
|
|
1418
2052
|
};
|
|
1419
2053
|
exports.testing = testing;
|
|
1420
2054
|
// VERSION is this package's version. It MUST equal package.json "version":
|
|
1421
2055
|
// the release orchestrator rewrites both, and the version test fails the
|
|
1422
2056
|
// build if they drift. Mirrors `const VERSION` in go/expr.go.
|
|
1423
|
-
const VERSION = '0.5.
|
|
2057
|
+
const VERSION = '0.5.20';
|
|
1424
2058
|
exports.VERSION = VERSION;
|
|
1425
2059
|
// The translation parts a host composes a translation from (admin ADR-27):
|
|
1426
2060
|
// the manifest and the alchemy sources, generated by ../embed-translate.js.
|