@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/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
|
-
//
|
|
77
|
-
evaluate?:
|
|
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
|
-
//
|
|
114
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
1704
|
-
|
|
1705
|
-
export {
|
|
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 {
|
|
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.
|