@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/src/expr.ts CHANGED
@@ -67,14 +67,36 @@ type OpDef = {
67
67
  required?: boolean
68
68
  allow?: string[]
69
69
  }
70
+
71
+ // STRUCTURAL (key) operator: an occurrence is a map that owns the member
72
+ // named by `src` (for example `$ref`), recognised by shape when a tree is
73
+ // folded, never by a token. No fixed token is registered, so the base
74
+ // grammar's pair rule keeps the key position, and no binding power
75
+ // applies, since a key operator never competes for an operand: its two
76
+ // terms are fixed by the shape, the member's value and the sibling map.
77
+ key?: boolean
78
+
79
+ // A key operator that a fold recognises and reports but never
80
+ // evaluates: `$dynamicRef`, whose meaning depends on a scope the parser
81
+ // does not have. Its map folds as a plain map, so operators inside it
82
+ // still reduce. A host that wants to look at such a site declares it as
83
+ // an ordinary key operator and returns `site.plain()` instead.
84
+ dynamic?: boolean
70
85
  }
71
86
 
72
87
  // Options for the plugin.
73
88
  type ExprOptions = {
74
89
  op?: { [name: string]: OpDef }
75
90
 
76
- // TODO: define Evalute type
77
- evaluate?: typeof evaluation
91
+ // Reduce each expression to a value as it closes.
92
+ evaluate?: Evaluate
93
+
94
+ // Fold the finished document before `parse` returns (deferred mode):
95
+ // the plugin subscribes to the engine's post-process rule event and
96
+ // replaces the root node with `fold(root, options.fold)` on the last
97
+ // pass of the parse. Key operators resolve against the finished root,
98
+ // where `evaluate` above reduces a token expression at its own close.
99
+ fold?: FoldOptions
78
100
  }
79
101
 
80
102
  // Full operator description (provided for evaluation).
@@ -103,6 +125,8 @@ type Op = {
103
125
  required: boolean
104
126
  allow?: string[]
105
127
  }
128
+ key: boolean
129
+ dynamic: boolean
106
130
  token: Token
107
131
  OP_MARK: typeof OP_MARK
108
132
  }
@@ -110,8 +134,106 @@ type Op = {
110
134
  // Lookup operators by token.
111
135
  type OpMap = { [tin: number]: Op }
112
136
 
113
- // Resolve the value of an operartion
114
- type Evaluate = (rule: Rule, ctx: Context, op: Op, ...terms: any) => any
137
+ // Lookup key operators by the member name that marks a site, in the
138
+ // order a map that owns two of them is resolved by: static operators
139
+ // before dynamic ones, then member names in code-point order.
140
+ type KeyMap = Map<string, Op>
141
+
142
+ // Resolve the value of an operation: `terms` is one array, the folded
143
+ // terms in order.
144
+ type Evaluate = (rule: Rule, ctx: Context, op: Op, terms: any[]) => any
145
+
146
+ // A location, as segments from a document's root: member names, and
147
+ // array indices as numbers or as RFC 6901 index strings (`0`, `12`).
148
+ type Path = (string | number)[]
149
+
150
+ // What `site.at` gives back: whether the location names anything,
151
+ // whether reaching it closed a cycle, and, only when it was found and is
152
+ // not a cycle, its folded value.
153
+ type FoldHit = { found: boolean; cycle: boolean; value?: any }
154
+
155
+ // What `fold` hands its callbacks for one map (or one token operator
156
+ // array): the verbatim root of the map's document and that document's
157
+ // key, the path to the map, the verbatim map, the bag inherited along the
158
+ // path (`inherit`, applied at every map from the root down to and
159
+ // including this one), the map's opening token when positions are
160
+ // recorded, and the ways to reach the rest of the tree: `at` folds
161
+ // another location, in this document or another, through any key-operator
162
+ // sites on the way; `locate` reads one raw; `plain` is this map folded as
163
+ // a plain map, member by member in its own order, which is what a
164
+ // resolver returns where the operator does not apply.
165
+ type Site = {
166
+ readonly root: any
167
+ readonly doc: string
168
+ readonly path: Path
169
+ readonly node: any
170
+ readonly k: Record<string, any>
171
+ readonly token?: Token
172
+ at: (path: Path, doc?: string) => FoldHit
173
+ locate: (path: Path, doc?: string) => { found: boolean; value?: any }
174
+ plain: () => any
175
+ }
176
+
177
+ // Reduce one site: a key operator with `[value, siblings]`, both already
178
+ // folded, or, when `tokens` is on, a token operator met in the walk with
179
+ // its folded terms.
180
+ type FoldEvaluate = (site: Site, op: Op, terms: any[]) => any
181
+
182
+ // The bag a map's subtree inherits, from the bag its parent's subtree
183
+ // sees. Called once per map on a path the fold needs, from the root
184
+ // down, so a site reached early through a reference gets ITS ancestors'
185
+ // context, never the referrer's: the engine's `k` discipline, for a
186
+ // finished tree. Inside the call, `site.k` is the parent's bag.
187
+ type Inherit = (site: Site, node: any, k: Record<string, any>) => Record<string, any>
188
+
189
+ // Whether a map that owns a key operator's member is an occurrence of it
190
+ // here. A map for which this is false folds as a plain map, with no
191
+ // `evaluate` call and no report row.
192
+ type Recognise = (site: Site, op: Op) => boolean
193
+
194
+ // The opening token of a container, by the container's identity.
195
+ type Positions = (node: object) => Token | undefined
196
+
197
+ // One report row: the site (`doc`, `at`) and the location it named
198
+ // (`targetDoc`, `target`), as document keys and RFC 6901 pointers.
199
+ type FoldEntry = { doc: string; at: string; targetDoc: string; target: string }
200
+
201
+ type FoldReport = {
202
+ cycles: FoldEntry[]
203
+ unresolved: FoldEntry[]
204
+ dynamic: { doc: string; at: string; src: string }[]
205
+ }
206
+
207
+ type FoldOptions = {
208
+ // The operator table; only the `key` entries matter to a fold. The
209
+ // plugin supplies its own table in deferred mode.
210
+ op?: { [name: string]: OpDef }
211
+ evaluate: FoldEvaluate
212
+ inherit?: Inherit
213
+ recognise?: Recognise
214
+ // The starting bag of every document's root.
215
+ k?: Record<string, any>
216
+ // The key of the document being folded. Default ''.
217
+ doc?: string
218
+ // Another document's raw root, by key, for `site.at(path, doc)`: called
219
+ // once per key, `undefined` when it cannot be had. Deciding what a key
220
+ // may name, and loading it, is the host's.
221
+ load?: (doc: string, site: Site) => any
222
+ // A cycle: tell the resolver (`cycle: true`), which is the default, or
223
+ // throw a FoldError.
224
+ cycle?: 'verbatim' | 'error'
225
+ // Reduce token-operator arrays met in the walk through `evaluate` too,
226
+ // so one fold handles `+` and `$ref`. Default true.
227
+ tokens?: boolean
228
+ // The opening token of each container, as a lookup by identity
229
+ // (`recordPositions` builds one); in deferred mode `true` records them
230
+ // during the parse.
231
+ positions?: boolean | Positions
232
+ // How deep the walk may go, nesting and reference hops counted
233
+ // together. Default 1000.
234
+ maxDepth?: number
235
+ report?: (report: FoldReport) => void
236
+ }
115
237
 
116
238
  // Mark Operator objects as owned by this plugin.
117
239
  const OP_MARK = {}
@@ -150,6 +272,8 @@ let Expr: Plugin = function Expr(tn: Tabnas, options: ExprOptions) {
150
272
  const infixTM: OpMap = makeOpMap(token, fixed, optop, 'infix')
151
273
  const ternaryTM: OpMap = makeOpMap(token, fixed, optop, 'ternary')
152
274
 
275
+ const keyTM: KeyMap = makeKeyMap(optop)
276
+
153
277
  const parenOTM: OpMap = makeParenMap(token, fixed, optop)
154
278
  const parenCTM: OpMap = omap(parenOTM, ([_, pdef]: [Tin, Op]) => [
155
279
  undefined,
@@ -183,6 +307,43 @@ let Expr: Plugin = function Expr(tn: Tabnas, options: ExprOptions) {
183
307
  },
184
308
  })
185
309
 
310
+ // Deferred mode: fold the finished root on the last pass of the parse.
311
+ // The engine's post-process rule event fires after every pass in every
312
+ // grammar; the last pass is a depth-0 close whose alternate neither
313
+ // pushes, replaces nor failed (a rule with no close alternates reports
314
+ // none), outside any probe and not synthesised by recovery. A parse
315
+ // that fails is therefore never folded: the engine reports the failing
316
+ // pass with `alt.err` set, from a handler that swallows subscriber
317
+ // errors. The start rule's own after-close is not that event: a grammar
318
+ // may replace its start rule (yaml's `stream` does on a directive or a
319
+ // document marker), and `parse` returns the original root rule's node,
320
+ // which is what is written here.
321
+ if (options.fold) {
322
+ const foldOptions = options.fold
323
+ const deferred: FoldOptions = {
324
+ ...foldOptions,
325
+ op: optop,
326
+ positions: true === foldOptions.positions ? recordPositions(tn) : foldOptions.positions,
327
+ }
328
+ tn.sub({
329
+ ruleDone: (r: Rule, ctx: Context, done: any) => {
330
+ if (0 !== r.d || 'c' !== done.state || done.probe || done.forced) return
331
+ const alt = done.alt
332
+ if (null != alt && (alt.p || alt.r || alt.err)) return
333
+ // A token the last close backtracked over is still in the
334
+ // lookahead buffer, and the engine's trailing-content check fails
335
+ // the parse on it once the rule loop ends: such a parse is not
336
+ // folded, so a fold error cannot stand in for the syntax error.
337
+ // Under recovery the parse returns its value all the same.
338
+ const t0 = ctx.t0
339
+ if (!ctx.cfg.parse.recover.enabled && null != t0 &&
340
+ ctx.NOTOKEN.tin !== t0.tin && ZZ !== t0.tin) return
341
+ const root = ctx.root()
342
+ root.node = foldTree(root.node, deferred, keyTM)
343
+ },
344
+ })
345
+ }
346
+
186
347
  // Append 'expr' to the g (group tag) of every alt added below. Mirrors
187
348
  // the jsonic grammar(...) setting {rule:{alt:{g:'expr'}}}, applied
188
349
  // manually because the plugin uses jsonic.rule() (not jsonic.grammar()).
@@ -1348,6 +1509,8 @@ function makeOpMap(
1348
1509
  active: false,
1349
1510
  required: false,
1350
1511
  },
1512
+ key: false,
1513
+ dynamic: false,
1351
1514
  token: {} as Token,
1352
1515
  OP_MARK,
1353
1516
  })
@@ -1426,6 +1589,8 @@ function makeParenMap(
1426
1589
  tkn: '',
1427
1590
  tin: -1,
1428
1591
  terms: 1,
1592
+ key: false,
1593
+ dynamic: false,
1429
1594
  token: {} as Token,
1430
1595
  OP_MARK,
1431
1596
  }
@@ -1434,6 +1599,593 @@ function makeParenMap(
1434
1599
  }, {}) as OpMap
1435
1600
  }
1436
1601
 
1602
+
1603
+ // Key operators, by the member name that marks a site. A key operator
1604
+ // carries no token, no tin and no binding power; `terms` is 2 (the
1605
+ // member's value, then the sibling map). The table is a Map in the order
1606
+ // a map that owns two of the members is resolved by: static operators
1607
+ // before dynamic ones, then member names in code-point order. Neither
1608
+ // declaration order nor object key order is used: a Go option table has
1609
+ // no declaration order, and a JavaScript object lists integer-like names
1610
+ // first. Two operators on one member name are refused.
1611
+ function makeKeyMap(optop: { [name: string]: OpDef }): KeyMap {
1612
+ const found: Op[] = []
1613
+ const owner = new Map<string, string>()
1614
+ for (const [name, kdef] of entries(optop) as [string, any][]) {
1615
+ if (!kdef || !kdef.key) continue
1616
+ const src = 'string' === typeof kdef.src ? kdef.src : (kdef.src || [])[0]
1617
+ if ('string' !== typeof src || '' === src) {
1618
+ throw new FoldError('fold_key', 'key operator ' + shown(name) + ' names no member')
1619
+ }
1620
+ const prior = owner.get(src)
1621
+ if (undefined !== prior) {
1622
+ throw new FoldError('fold_key',
1623
+ 'key operators ' + shown(prior) + ' and ' + shown(name) + ' both name the member ' + shown(src))
1624
+ }
1625
+ owner.set(src, name)
1626
+ found.push({
1627
+ name: name + (name.endsWith('-key') ? '' : '-key'),
1628
+ src,
1629
+ left: Number.MIN_SAFE_INTEGER,
1630
+ right: Number.MAX_SAFE_INTEGER,
1631
+ use: kdef.use || {},
1632
+ prefix: false,
1633
+ suffix: false,
1634
+ infix: false,
1635
+ ternary: false,
1636
+ paren: false,
1637
+ terms: 2,
1638
+ tkn: '',
1639
+ tin: -1,
1640
+ osrc: '',
1641
+ csrc: '',
1642
+ otkn: '',
1643
+ otin: -1,
1644
+ ctkn: '',
1645
+ ctin: -1,
1646
+ preval: { active: false, required: false },
1647
+ key: true,
1648
+ dynamic: !!kdef.dynamic,
1649
+ token: {} as Token,
1650
+ OP_MARK,
1651
+ })
1652
+ }
1653
+ found.sort((a, b) => a.dynamic === b.dynamic ? byCodePoint(a.src, b.src) : a.dynamic ? 1 : -1)
1654
+ return new Map(found.map((op) => [op.src, op]))
1655
+ }
1656
+
1657
+ // Compare two strings by Unicode code point, which is the order Go and
1658
+ // Rust compare strings in; JavaScript's own `<` compares UTF-16 code
1659
+ // units, which differs above U+FFFF.
1660
+ function byCodePoint(a: string, b: string): number {
1661
+ const x = Array.from(a)
1662
+ const y = Array.from(b)
1663
+ for (let i = 0; i < x.length && i < y.length; i++) {
1664
+ const d = (x[i].codePointAt(0) as number) - (y[i].codePointAt(0) as number)
1665
+ if (0 !== d) return d
1666
+ }
1667
+ return x.length - y.length
1668
+ }
1669
+
1670
+
1671
+ // A plain record: an object whose prototype is `null` (the engine's
1672
+ // maps) or `Object.prototype` (`JSON.parse`, a literal). Anything else, a
1673
+ // Map, a Date, a class instance, a boxed value, is a leaf to a fold and
1674
+ // to `evaluation`: passed through by identity, never rebuilt, since a
1675
+ // rebuilt copy would carry the prototype without the internal state.
1676
+ function isRecord(v: any): boolean {
1677
+ if (null == v || 'object' !== typeof v || Array.isArray(v)) return false
1678
+ const proto = Object.getPrototypeOf(v)
1679
+ return null === proto || Object.prototype === proto
1680
+ }
1681
+
1682
+ // The key operator a map is an occurrence of, by shape: the first, in the
1683
+ // table's order, whose member the map owns. Undefined for anything else.
1684
+ function keyOpOf(keys: KeyMap, node: any): Op | undefined {
1685
+ if (!isRecord(node)) return undefined
1686
+ for (const op of keys.values()) {
1687
+ if (Object.prototype.hasOwnProperty.call(node, op.src)) return op
1688
+ }
1689
+ return undefined
1690
+ }
1691
+
1692
+ // RFC 6901: a pointer from path segments.
1693
+ function pointer(path: Path): string {
1694
+ let out = ''
1695
+ for (const seg of path) {
1696
+ out += '/' + String(seg).replace(/~/g, '~0').replace(/\//g, '~1')
1697
+ }
1698
+ return out
1699
+ }
1700
+
1701
+ // An RFC 6901 array index: `0`, or digits with no leading zero, given as
1702
+ // a string, or a non-negative integer; -1 for anything else, `-` and
1703
+ // out-of-range indices included.
1704
+ const INDEX = /^(0|[1-9][0-9]*)$/
1705
+ function arrayIndex(seg: string | number, length: number): number {
1706
+ const i = 'number' === typeof seg
1707
+ ? (Number.isInteger(seg) ? seg : -1)
1708
+ : (INDEX.test(seg) ? Number(seg) : -1)
1709
+ return 0 <= i && i < length ? i : -1
1710
+ }
1711
+
1712
+ // One step of a path: an index into a list, or an own member of a plain
1713
+ // record. NOTHING when the step names nothing.
1714
+ const NOTHING = Symbol('nothing')
1715
+ function step(node: any, seg: string | number): any {
1716
+ if (Array.isArray(node)) {
1717
+ const i = arrayIndex(seg, node.length)
1718
+ return i < 0 ? NOTHING : node[i]
1719
+ }
1720
+ if (isRecord(node) && Object.prototype.hasOwnProperty.call(node, seg)) return node[seg]
1721
+ return NOTHING
1722
+ }
1723
+
1724
+ // A new container of the same prototype as `like` (the engine builds
1725
+ // null-prototype objects), with members DEFINED rather than assigned so a
1726
+ // member named `__proto__` stays data.
1727
+ function newRecord(like: any): any {
1728
+ return Object.create(Object.getPrototypeOf(like))
1729
+ }
1730
+ function define(o: any, k: string, v: any): void {
1731
+ Object.defineProperty(o, k, { value: v, enumerable: true, writable: true, configurable: true })
1732
+ }
1733
+
1734
+
1735
+ // The fold's own errors. `code` is what a fixture pins: `fold_cycle` (a
1736
+ // cycle under `cycle: 'error'`), `fold_depth` (nesting and reference hops
1737
+ // past `maxDepth`, or the stack ran out first), `fold_inherit` (an
1738
+ // `inherit` that returned no bag, or folded its own subtree), `fold_key`
1739
+ // (an operator table with two key operators on one member name, or one
1740
+ // on none) and `fold_option` (`positions: true` outside deferred mode).
1741
+ // The document key and the pointers are fields; the message quotes them
1742
+ // JSON-escaped and cut short, since member names are document content.
1743
+ class FoldError extends Error {
1744
+ code: string
1745
+ doc?: string
1746
+ at?: string
1747
+ targetDoc?: string
1748
+ target?: string
1749
+ constructor(
1750
+ code: string,
1751
+ message: string,
1752
+ fields?: { doc?: string; at?: string; targetDoc?: string; target?: string },
1753
+ ) {
1754
+ super('expr fold: ' + message)
1755
+ this.name = 'FoldError'
1756
+ this.code = code
1757
+ if (fields) Object.assign(this, fields)
1758
+ }
1759
+ }
1760
+
1761
+ // A key or pointer for a message: JSON-escaped, so a newline or an escape
1762
+ // sequence in a member name stays visible, and at most about 80
1763
+ // characters long.
1764
+ function shown(s: string): string {
1765
+ const q = JSON.stringify(s)
1766
+ return q.length <= 80 ? q : q.slice(0, 76) + '..."'
1767
+ }
1768
+
1769
+
1770
+ // Record the opening token of every container a parse builds, by the
1771
+ // container's identity, and return the lookup `fold` takes as
1772
+ // `positions`. It subscribes to the engine's post-process rule event on
1773
+ // the instance, so it works in any grammar; the table is weak, so a
1774
+ // parsed tree's entries go with the tree.
1775
+ function recordPositions(tn: Tabnas): Positions {
1776
+ const tokens = new WeakMap<object, Token>()
1777
+ tn.sub({
1778
+ ruleDone: (r: Rule, _ctx: Context, done: any) => {
1779
+ if ('o' !== done.state) return
1780
+ const node: any = r.node
1781
+ if (null != node && 'object' === typeof node && !tokens.has(node) && r.o0) {
1782
+ tokens.set(node, r.o0)
1783
+ }
1784
+ },
1785
+ })
1786
+ return (node: object) => tokens.get(node)
1787
+ }
1788
+
1789
+
1790
+ // The fold's memory: one node per container location the fold has
1791
+ // reached, in a trie that follows the document, so a location's memo,
1792
+ // state and inherited bag cost a constant amount however deep it is.
1793
+ // Pointers are rendered only for report rows and errors.
1794
+ type FoldDoc = { key: string; root: any; top: PNode }
1795
+
1796
+ class PNode {
1797
+ kids: Map<string, PNode> | undefined = undefined
1798
+ // 0 not yet folded, 1 being folded (on the active stack), 2 folded.
1799
+ state = 0
1800
+ value: any = undefined
1801
+ // How the walk entered it: 0 from its parent, 1 through a reference.
1802
+ via = 0
1803
+ k: Record<string, any> | undefined = undefined
1804
+ kBusy = false
1805
+ // Its resolution met a cycle, so the resolver was told `cycle: true`.
1806
+ cut = false
1807
+ // A member of a loop of references with no container in it, or a
1808
+ // reference into one: there is nothing to resolve it to.
1809
+ loop = false
1810
+ // The key operator this map is an occurrence of, decided once (shape,
1811
+ // then the host's `recognise`): undefined until decided, null for none.
1812
+ site: Op | null | undefined = undefined
1813
+ parts: { op: Op; value: any; siblings: any } | undefined = undefined
1814
+ plainDone = false
1815
+ plainValue: any = undefined
1816
+ ptr: string | undefined = undefined
1817
+ constructor(
1818
+ public doc: FoldDoc,
1819
+ public parent: PNode | undefined,
1820
+ public seg: string | number,
1821
+ public raw: any,
1822
+ ) {}
1823
+ }
1824
+
1825
+
1826
+ // Fold a finished tree: walk it, reduce every key-operator site
1827
+ // bottom-up through `opts.evaluate`, and return a NEW tree; the input is
1828
+ // never changed (though a value a resolver returns, `site.node` among
1829
+ // them, is placed in the output as it is). Values flow up through
1830
+ // `evaluate`; context flows down through `inherit`, computed per path
1831
+ // from each map's own ancestors and kept. One result per location, so
1832
+ // every site naming one target shares one folded value. Every container
1833
+ // being folded is on an active stack: a site whose target is on it
1834
+ // closes a cycle, which the resolver is told about (or which throws).
1835
+ // Dynamic key operators are reported and folded as plain maps. Token
1836
+ // operator arrays met in the walk are reduced through the same
1837
+ // `evaluate` when `tokens` is on.
1838
+ function fold(root: any, opts: FoldOptions): any {
1839
+ return foldTree(root, opts, makeKeyMap(opts.op || {}))
1840
+ }
1841
+
1842
+ function foldTree(root: any, opts: FoldOptions, keys: KeyMap): any {
1843
+ if (true === opts.positions) {
1844
+ throw new FoldError('fold_option',
1845
+ 'positions: true records tokens during a parse, so it works in the deferred mode only;'
1846
+ + ' pass a lookup, such as recordPositions(tn), to fold')
1847
+ }
1848
+
1849
+ const tokens = false !== opts.tokens
1850
+ const inherit = opts.inherit
1851
+ const recognise = opts.recognise
1852
+ const lookup = 'function' === typeof opts.positions ? opts.positions : undefined
1853
+ const maxDepth = null == opts.maxDepth ? 1000 : opts.maxDepth
1854
+ const base = opts.k || {}
1855
+ const report: FoldReport = { cycles: [], unresolved: [], dynamic: [] }
1856
+ const noted = new Set<string>()
1857
+ const stack: PNode[] = []
1858
+ const docs = new Map<string, FoldDoc | null>()
1859
+
1860
+ const makeDoc = (key: string, raw: any): FoldDoc => {
1861
+ const doc = { key, root: raw } as FoldDoc
1862
+ doc.top = new PNode(doc, undefined, '', raw)
1863
+ docs.set(key, doc)
1864
+ return doc
1865
+ }
1866
+
1867
+ // Another document, by key, loaded once; undefined when it cannot be had.
1868
+ const docOf = (from: PNode, key?: string): FoldDoc | undefined => {
1869
+ if (undefined === key || key === from.doc.key) return from.doc
1870
+ let doc = docs.get(key)
1871
+ if (undefined === doc) {
1872
+ const raw = undefined === opts.load ? undefined : opts.load(key, siteOf(from))
1873
+ doc = undefined === raw ? null : makeDoc(key, raw)
1874
+ docs.set(key, doc)
1875
+ }
1876
+ return null === doc ? undefined : doc
1877
+ }
1878
+
1879
+ const kid = (pn: PNode, seg: string | number, raw: any): PNode => {
1880
+ const name = String(seg)
1881
+ let kids = pn.kids
1882
+ if (undefined === kids) kids = pn.kids = new Map()
1883
+ let node = kids.get(name)
1884
+ if (undefined === node) {
1885
+ node = new PNode(pn.doc, pn, seg, raw)
1886
+ kids.set(name, node)
1887
+ }
1888
+ return node
1889
+ }
1890
+
1891
+ const ptrOf = (pn: PNode): string => {
1892
+ if (undefined !== pn.ptr) return pn.ptr
1893
+ const chain: PNode[] = []
1894
+ let p: PNode | undefined = pn
1895
+ while (undefined !== p && undefined === p.ptr) {
1896
+ chain.push(p)
1897
+ p = p.parent
1898
+ }
1899
+ let s = undefined === p ? '' : (p.ptr as string)
1900
+ for (let i = chain.length - 1; 0 <= i; i--) {
1901
+ const c = chain[i]
1902
+ s = undefined === c.parent ? '' : s + '/' + String(c.seg).replace(/~/g, '~0').replace(/\//g, '~1')
1903
+ c.ptr = s
1904
+ }
1905
+ return s
1906
+ }
1907
+
1908
+ const pathOf = (pn: PNode): Path => {
1909
+ const path: Path = []
1910
+ for (let p: PNode | undefined = pn; undefined !== p && undefined !== p.parent; p = p.parent) {
1911
+ path.push(p.seg)
1912
+ }
1913
+ return path.reverse()
1914
+ }
1915
+
1916
+ const note = (list: 'cycles' | 'unresolved' | 'dynamic', row: any) => {
1917
+ const id = JSON.stringify([list, row.doc, row.at, row.targetDoc, row.target, row.src])
1918
+ if (noted.has(id)) return
1919
+ noted.add(id)
1920
+ report[list].push(row)
1921
+ }
1922
+
1923
+ // The bag at a map: the `inherit` calls of its ancestors and of the map
1924
+ // itself, from the root down, each kept, worked out without recursion.
1925
+ const kOf = (pn: PNode): Record<string, any> => {
1926
+ if (undefined === inherit) return base
1927
+ if (undefined !== pn.k) return pn.k
1928
+ const chain: PNode[] = []
1929
+ let p: PNode | undefined = pn
1930
+ while (undefined !== p && undefined === p.k) {
1931
+ if (p.kBusy) {
1932
+ throw new FoldError('fold_inherit', 'inherit folded its own subtree at ' + shown(ptrOf(p)),
1933
+ { doc: p.doc.key, at: ptrOf(p) })
1934
+ }
1935
+ chain.push(p)
1936
+ p = p.parent
1937
+ }
1938
+ let k: Record<string, any> = undefined === p ? base : (p.k as Record<string, any>)
1939
+ for (let i = chain.length - 1; 0 <= i; i--) {
1940
+ const c = chain[i]
1941
+ if (isRecord(c.raw)) {
1942
+ c.kBusy = true
1943
+ let next: any
1944
+ try {
1945
+ next = inherit(siteOf(c, k), c.raw, k)
1946
+ }
1947
+ finally {
1948
+ c.kBusy = false
1949
+ }
1950
+ if (null == next || 'object' !== typeof next) {
1951
+ throw new FoldError('fold_inherit', 'inherit returned no bag at ' + shown(ptrOf(c)),
1952
+ { doc: c.doc.key, at: ptrOf(c) })
1953
+ }
1954
+ k = next
1955
+ }
1956
+ c.k = k
1957
+ }
1958
+ return k
1959
+ }
1960
+
1961
+ const siteOf = (pn: PNode, parentK?: Record<string, any>): Site => {
1962
+ let path: Path | undefined
1963
+ const node = pn.raw
1964
+ return {
1965
+ root: pn.doc.root,
1966
+ doc: pn.doc.key,
1967
+ get path() {
1968
+ return undefined === path ? (path = pathOf(pn)) : path
1969
+ },
1970
+ node,
1971
+ get k() {
1972
+ return undefined === parentK ? kOf(pn) : parentK
1973
+ },
1974
+ token: undefined !== lookup && null != node && 'object' === typeof node ? lookup(node) : undefined,
1975
+ at: (target: Path, doc?: string) => at(pn, target, doc),
1976
+ locate: (target: Path, doc?: string) => locate(pn, target, doc),
1977
+ plain: () => plainOf(pn),
1978
+ }
1979
+ }
1980
+
1981
+ const locate = (from: PNode, target: Path, key?: string): { found: boolean; value?: any } => {
1982
+ const doc = docOf(from, key)
1983
+ if (undefined === doc) return { found: false }
1984
+ let node = doc.root
1985
+ for (const seg of target) {
1986
+ node = step(node, seg)
1987
+ if (NOTHING === node) return { found: false }
1988
+ }
1989
+ return { found: true, value: node }
1990
+ }
1991
+
1992
+ const miss = (from: PNode, targetDoc: string, target: Path): FoldHit => {
1993
+ note('unresolved', { doc: from.doc.key, at: ptrOf(from), targetDoc, target: pointer(target) })
1994
+ return { found: false, cycle: false }
1995
+ }
1996
+
1997
+ // A cycle: `from` named `target` while `target` was still being folded
1998
+ // (a back edge), or `target` is in a loop of references. Every member of
1999
+ // a loop made only of references is a cycle, and so is every reference
2000
+ // into one, since none of them has anything to resolve to. A cycle that
2001
+ // passes through a container is cut once, at the site whose target was
2002
+ // being folded; a later reference to that site gets its folded value.
2003
+ const cycleTo = (from: PNode, target: PNode): FoldHit => {
2004
+ const row = { doc: from.doc.key, at: ptrOf(from), targetDoc: target.doc.key, target: ptrOf(target) }
2005
+ if ('error' === opts.cycle) {
2006
+ throw new FoldError('fold_cycle', 'cycle at ' + shown(row.at) + ' to ' + shown(row.target), row)
2007
+ }
2008
+ note('cycles', row)
2009
+ from.cut = true
2010
+ if (1 === target.state) {
2011
+ const i = stack.lastIndexOf(target)
2012
+ let refs = 0 <= i
2013
+ for (let j = i + 1; refs && j < stack.length; j++) refs = 1 === stack[j].via
2014
+ if (refs) {
2015
+ for (let j = i; j < stack.length; j++) stack[j].loop = true
2016
+ from.loop = true
2017
+ }
2018
+ }
2019
+ else if (target.loop) {
2020
+ from.loop = true
2021
+ }
2022
+ return { found: true, cycle: true }
2023
+ }
2024
+
2025
+ // The folded value at `target` in a document, for the site at `from`.
2026
+ // Raw members are followed from the root; a key-operator map met on the
2027
+ // way is folded (through the memo) and the remaining segments read its
2028
+ // folded value, so a reference to a reference resolves. A target still
2029
+ // being folded is a cycle, and so is a path that must pass through a
2030
+ // site that was cut.
2031
+ const at = (from: PNode, target: Path, key?: string): FoldHit => {
2032
+ const targetDoc = undefined === key ? from.doc.key : key
2033
+ const doc = docOf(from, key)
2034
+ if (undefined === doc) return miss(from, targetDoc, target)
2035
+ let pn = doc.top
2036
+ let node: any = doc.root
2037
+ for (let i = 0; i < target.length; i++) {
2038
+ const v = step(node, target[i])
2039
+ if (NOTHING === v) return miss(from, targetDoc, target)
2040
+ if (i === target.length - 1 && !Array.isArray(v) && !isRecord(v)) {
2041
+ return { found: true, cycle: false, value: v }
2042
+ }
2043
+ pn = kid(pn, Array.isArray(node) ? arrayIndex(target[i], node.length) : String(target[i]), v)
2044
+ node = v
2045
+ // Only a static site changes what lies below it; any other map,
2046
+ // a dynamic one or one `recognise` refused, folds member by member,
2047
+ // so the raw path below it names the same location.
2048
+ const op = i < target.length - 1 && isRecord(node) ? siteOpOf(pn) : null
2049
+ if (null !== op && !op.dynamic) {
2050
+ if (1 === pn.state || pn.loop) return cycleTo(from, pn)
2051
+ let folded = walk(pn, 1)
2052
+ if (pn.loop || pn.cut) return cycleTo(from, pn)
2053
+ for (i++; i < target.length; i++) {
2054
+ folded = step(folded, target[i])
2055
+ if (NOTHING === folded) return miss(from, targetDoc, target)
2056
+ }
2057
+ return { found: true, cycle: false, value: folded }
2058
+ }
2059
+ }
2060
+ if (1 === pn.state) return cycleTo(from, pn)
2061
+ const value = walk(pn, 1)
2062
+ if (pn.loop) return cycleTo(from, pn)
2063
+ return { found: true, cycle: false, value }
2064
+ }
2065
+
2066
+ // The key operator a map is an occurrence of here, if any: its shape,
2067
+ // then the host's `recognise`, decided once per location.
2068
+ const siteOpOf = (pn: PNode): Op | null => {
2069
+ if (undefined !== pn.site) return pn.site
2070
+ let op: Op | null = keyOpOf(keys, pn.raw) ?? null
2071
+ pn.site = op
2072
+ if (null !== op && undefined !== recognise && !recognise(siteOf(pn), op)) op = pn.site = null
2073
+ return op
2074
+ }
2075
+
2076
+ // A member's folded value: a container through the memo, anything else
2077
+ // as it is.
2078
+ const member = (pn: PNode, seg: string | number, child: any): any =>
2079
+ Array.isArray(child) || isRecord(child) ? walk(kid(pn, seg, child), 0) : child
2080
+
2081
+ // A map folded as a plain map, members in its own order.
2082
+ const plainOf = (pn: PNode): any => {
2083
+ if (pn.plainDone) return pn.plainValue
2084
+ const node = pn.raw
2085
+ if (!isRecord(node)) return walk(pn, 0)
2086
+ const parts = pn.parts
2087
+ const out = newRecord(node)
2088
+ for (const name of Object.keys(node)) {
2089
+ define(out, name,
2090
+ undefined === parts ? member(pn, name, node[name])
2091
+ : name === parts.op.src ? parts.value : parts.siblings[name])
2092
+ }
2093
+ pn.plainDone = true
2094
+ pn.plainValue = out
2095
+ return out
2096
+ }
2097
+
2098
+ const walk = (pn: PNode, via: number): any => {
2099
+ if (2 === pn.state) return pn.value
2100
+ const node = pn.raw
2101
+ const list = Array.isArray(node)
2102
+ if (!list && (!isRecord(node) || OP_MARK === node.OP_MARK)) return node
2103
+ if (1 === pn.state) return node
2104
+ if (maxDepth <= stack.length) {
2105
+ throw new FoldError('fold_depth',
2106
+ 'nesting and references deeper than ' + maxDepth + ' at ' + shown(ptrOf(pn)),
2107
+ { doc: pn.doc.key, at: ptrOf(pn) })
2108
+ }
2109
+ pn.state = 1
2110
+ pn.via = via
2111
+ stack.push(pn)
2112
+ let out: any
2113
+ try {
2114
+ if (list && isOp(node)) {
2115
+ // Each term is folded at its own path first, so a key-operator
2116
+ // site inside a token expression resolves, and a nested
2117
+ // expression is kept by path. With `tokens` off the operator
2118
+ // node is rebuilt around its own head, so it stays an operator.
2119
+ const terms: any[] = []
2120
+ for (let i = 1; i < node.length; i++) terms.push(member(pn, i, node[i]))
2121
+ out = tokens ? opts.evaluate(siteOf(pn), node[0], terms) : [node[0], ...terms]
2122
+ }
2123
+ else if (list) {
2124
+ // Members are walked inline, not through `member`, so a level of
2125
+ // nesting costs one stack frame.
2126
+ out = []
2127
+ for (let i = 0; i < node.length; i++) {
2128
+ const c = node[i]
2129
+ out.push(Array.isArray(c) || isRecord(c) ? walk(kid(pn, i, c), 0) : c)
2130
+ }
2131
+ }
2132
+ else {
2133
+ const op = siteOpOf(pn)
2134
+ if (null === op || op.dynamic) {
2135
+ if (null !== op) note('dynamic', { doc: pn.doc.key, at: ptrOf(pn), src: op.src })
2136
+ if (pn.plainDone) {
2137
+ out = pn.plainValue
2138
+ }
2139
+ else {
2140
+ out = newRecord(node)
2141
+ for (const name of Object.keys(node)) {
2142
+ const c = node[name]
2143
+ define(out, name, Array.isArray(c) || isRecord(c) ? walk(kid(pn, name, c), 0) : c)
2144
+ }
2145
+ pn.plainDone = true
2146
+ pn.plainValue = out
2147
+ }
2148
+ }
2149
+ else {
2150
+ const siblings = newRecord(node)
2151
+ let value: any
2152
+ for (const name of Object.keys(node)) {
2153
+ const v = member(pn, name, node[name])
2154
+ if (name === op.src) value = v
2155
+ else define(siblings, name, v)
2156
+ }
2157
+ pn.parts = { op, value, siblings }
2158
+ const site = siteOf(pn)
2159
+ out = opts.evaluate(site, { ...op, token: site.token || op.token }, [value, siblings])
2160
+ }
2161
+ }
2162
+ }
2163
+ finally {
2164
+ stack.pop()
2165
+ pn.state = 0
2166
+ }
2167
+ pn.state = 2
2168
+ pn.value = out
2169
+ return out
2170
+ }
2171
+
2172
+ const main = makeDoc(null == opts.doc ? '' : opts.doc, root)
2173
+ let out: any
2174
+ try {
2175
+ out = walk(main.top, 0)
2176
+ }
2177
+ catch (err: any) {
2178
+ if (err instanceof RangeError && /call stack/i.test(String(err.message))) {
2179
+ throw new FoldError('fold_depth',
2180
+ 'the stack ran out before maxDepth (' + maxDepth + ') was reached; lower maxDepth',
2181
+ { doc: main.key })
2182
+ }
2183
+ throw err
2184
+ }
2185
+ if (opts.report) opts.report(report)
2186
+ return out
2187
+ }
2188
+
1437
2189
  // Default operator precedence — the "binding power" scale.
1438
2190
  //
1439
2191
  // Binding powers are compared only by ORDER, never by magnitude (every
@@ -1593,7 +2345,13 @@ function prattify(expr: any, op?: Op, whence?: string): any[] {
1593
2345
  const EVALUATED = new WeakSet<any>()
1594
2346
 
1595
2347
 
1596
- function evaluation(rule: Rule, ctx: Context, expr: any, evaluate: Evaluate) {
2348
+ function evaluation(
2349
+ rule: Rule,
2350
+ ctx: Context,
2351
+ expr: any,
2352
+ evaluate: Evaluate,
2353
+ opts?: { objects?: boolean },
2354
+ ) {
1597
2355
  let out = expr ?? null
1598
2356
 
1599
2357
  if (null == expr || EVALUATED.has(expr)) {
@@ -1605,7 +2363,7 @@ function evaluation(rule: Rule, ctx: Context, expr: any, evaluate: Evaluate) {
1605
2363
  rule,
1606
2364
  ctx,
1607
2365
  expr[0],
1608
- expr.slice(1).map((term: any) => evaluation(rule, ctx, term, evaluate)),
2366
+ expr.slice(1).map((term: any) => evaluation(rule, ctx, term, evaluate, opts)),
1609
2367
  )
1610
2368
  }
1611
2369
 
@@ -1618,10 +2376,23 @@ function evaluation(rule: Rule, ctx: Context, expr: any, evaluate: Evaluate) {
1618
2376
  // the expr rule's close hook does that itself — mutating its own tree
1619
2377
  // is the parser's business, not this function's.
1620
2378
  else if (Array.isArray(expr)) {
1621
- out = expr.map((term: any) => evaluation(rule, ctx, term, evaluate))
2379
+ out = expr.map((term: any) => evaluation(rule, ctx, term, evaluate, opts))
1622
2380
  }
1623
2381
 
1624
- if (null != out && 'object' === typeof out) {
2382
+ // A plain object is passed through unchanged by default, as the
2383
+ // reference documents; with `objects` on it is reduced member by member
2384
+ // into a NEW object of the same prototype, which is what the eager
2385
+ // `evaluate` option already does for an expression inside a map.
2386
+ else if (opts && opts.objects && isRecord(expr)) {
2387
+ const o = newRecord(expr)
2388
+ for (const k of Object.keys(expr)) define(o, k, evaluation(rule, ctx, expr[k], evaluate, opts))
2389
+ out = o
2390
+ }
2391
+
2392
+ // Only a value this fold PRODUCED is marked: a node passed through
2393
+ // unchanged (a plain object, by default) is not, so a later fold with
2394
+ // `objects` on still reaches the operators inside it.
2395
+ if (null != out && 'object' === typeof out && out !== expr) {
1625
2396
  EVALUATED.add(out)
1626
2397
  }
1627
2398
 
@@ -1692,19 +2463,58 @@ function parseSimplified(tn: Tabnas, src: string, meta?: any): any {
1692
2463
  }
1693
2464
 
1694
2465
 
2466
+ // Mark a hand-built operator description as this plugin's, so `isOp` and
2467
+ // `evaluation` recognise a node headed by it. A sibling plugin that builds
2468
+ // operator nodes needs this; `simplify` needs it not, reading any
2469
+ // `src`-headed array. Fills nothing else: pass a complete description.
2470
+ function opify<T extends object>(op: T): T {
2471
+ ;(op as any).OP_MARK = OP_MARK
2472
+ return op
2473
+ }
2474
+
1695
2475
  const testing = {
1696
2476
  prattify,
1697
- opify: (x: any) => ((x.OP_MARK = OP_MARK), x),
2477
+ opify,
1698
2478
  }
1699
2479
 
1700
2480
  // VERSION is this package's version. It MUST equal package.json "version":
1701
2481
  // the release orchestrator rewrites both, and the version test fails the
1702
2482
  // build if they drift. Mirrors `const VERSION` in go/expr.go.
1703
- const VERSION = '0.5.18'
1704
-
1705
- export { VERSION, Expr, evaluation, simplify, parseSimplified, testing }
2483
+ const VERSION = '0.5.20'
2484
+
2485
+ export {
2486
+ VERSION,
2487
+ Expr,
2488
+ evaluation,
2489
+ simplify,
2490
+ parseSimplified,
2491
+ fold,
2492
+ recordPositions,
2493
+ FoldError,
2494
+ opify,
2495
+ isOp,
2496
+ keyOpOf,
2497
+ makeKeyMap,
2498
+ testing,
2499
+ }
1706
2500
 
1707
- export type { ExprOptions, OpDef, Op, Evaluate }
2501
+ export type {
2502
+ ExprOptions,
2503
+ OpDef,
2504
+ Op,
2505
+ Evaluate,
2506
+ FoldOptions,
2507
+ FoldEvaluate,
2508
+ FoldHit,
2509
+ FoldEntry,
2510
+ Inherit,
2511
+ Recognise,
2512
+ Positions,
2513
+ Site,
2514
+ FoldReport,
2515
+ Path,
2516
+ KeyMap,
2517
+ }
1708
2518
 
1709
2519
  // The translation parts a host composes a translation from (admin ADR-27):
1710
2520
  // the manifest and the alchemy sources, generated by ../embed-translate.js.