@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.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
- if (null != out && 'object' === typeof out) {
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: (x) => ((x.OP_MARK = OP_MARK), x),
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.18';
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.