aontu 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +2 -2
  2. package/dist/agentsmd.js +1 -1
  3. package/dist/alias.d.ts +3 -0
  4. package/dist/alias.js +59 -0
  5. package/dist/alias.js.map +1 -0
  6. package/dist/aontu.d.ts +5 -2
  7. package/dist/aontu.js +11 -3
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +8 -2
  10. package/dist/cli.js +564 -21
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +2 -0
  13. package/dist/ctx.js +1 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/escape.d.ts +5 -0
  16. package/dist/escape.js +455 -0
  17. package/dist/escape.js.map +1 -0
  18. package/dist/format.d.ts +9 -0
  19. package/dist/format.js +550 -55
  20. package/dist/format.js.map +1 -1
  21. package/dist/hints.js +74 -9
  22. package/dist/hints.js.map +1 -1
  23. package/dist/lang.js +374 -58
  24. package/dist/lang.js.map +1 -1
  25. package/dist/lower.d.ts +20 -0
  26. package/dist/lower.js +575 -0
  27. package/dist/lower.js.map +1 -0
  28. package/dist/lsp.d.ts +1 -1
  29. package/dist/lsp.js +4 -4
  30. package/dist/lsp.js.map +1 -1
  31. package/dist/mcp-server.js +2 -2
  32. package/dist/mcp-server.js.map +1 -1
  33. package/dist/mcp.d.ts +1 -0
  34. package/dist/mcp.js +40 -3
  35. package/dist/mcp.js.map +1 -1
  36. package/dist/mod-tool.js +8 -7
  37. package/dist/mod-tool.js.map +1 -1
  38. package/dist/mod.js +6 -6
  39. package/dist/mod.js.map +1 -1
  40. package/dist/render.d.ts +53 -0
  41. package/dist/render.js +542 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/sigdecl.js +1 -1
  44. package/dist/sigdecl.js.map +1 -1
  45. package/dist/std.d.ts +2 -0
  46. package/dist/std.js +498 -2
  47. package/dist/std.js.map +1 -1
  48. package/dist/template.d.ts +5 -0
  49. package/dist/template.js +257 -0
  50. package/dist/template.js.map +1 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/dist/unify.js +43 -0
  53. package/dist/unify.js.map +1 -1
  54. package/dist/val/AggFuncVal.d.ts +1 -1
  55. package/dist/val/AggFuncVal.js +10 -21
  56. package/dist/val/AggFuncVal.js.map +1 -1
  57. package/dist/val/BagVal.js +1 -1
  58. package/dist/val/BagVal.js.map +1 -1
  59. package/dist/val/ConstraintVal.js +1 -1
  60. package/dist/val/EachFuncVal.d.ts +1 -1
  61. package/dist/val/EachFuncVal.js +9 -15
  62. package/dist/val/EachFuncVal.js.map +1 -1
  63. package/dist/val/EmitFuncVal.d.ts +42 -0
  64. package/dist/val/EmitFuncVal.js +531 -0
  65. package/dist/val/EmitFuncVal.js.map +1 -0
  66. package/dist/val/FilterFuncVal.js +9 -6
  67. package/dist/val/FilterFuncVal.js.map +1 -1
  68. package/dist/val/FormFuncVal.d.ts +14 -0
  69. package/dist/val/FormFuncVal.js +55 -0
  70. package/dist/val/FormFuncVal.js.map +1 -0
  71. package/dist/val/FuncBaseVal.d.ts +1 -0
  72. package/dist/val/FuncBaseVal.js +16 -0
  73. package/dist/val/FuncBaseVal.js.map +1 -1
  74. package/dist/val/MapVal.d.ts +2 -1
  75. package/dist/val/MapVal.js +2 -1
  76. package/dist/val/MapVal.js.map +1 -1
  77. package/dist/val/PackFuncVal.d.ts +1 -1
  78. package/dist/val/PackFuncVal.js +23 -20
  79. package/dist/val/PackFuncVal.js.map +1 -1
  80. package/dist/val/PlaceVal.d.ts +3 -1
  81. package/dist/val/PlaceVal.js +9 -6
  82. package/dist/val/PlaceVal.js.map +1 -1
  83. package/dist/val/RefVal.d.ts +3 -0
  84. package/dist/val/RefVal.js +156 -17
  85. package/dist/val/RefVal.js.map +1 -1
  86. package/dist/val/StrFuncVal.d.ts +32 -0
  87. package/dist/val/StrFuncVal.js +292 -0
  88. package/dist/val/StrFuncVal.js.map +1 -0
  89. package/dist/val/Val.d.ts +7 -1
  90. package/dist/val/Val.js +12 -1
  91. package/dist/val/Val.js.map +1 -1
  92. package/dist/val/members.d.ts +9 -0
  93. package/dist/val/members.js +52 -0
  94. package/dist/val/members.js.map +1 -0
  95. package/grammar/aontu.abnf +4 -3
  96. package/grammar/aontu.gbnf +4 -3
  97. package/grammar/aontu.lark +4 -3
  98. package/grammar/aontu.tmLanguage.json +1 -1
  99. package/package.json +1 -1
  100. package/skill/SKILL.md +4 -4
  101. package/skill/error-codes.md +1 -1
  102. package/skill/examples.md +1 -1
  103. package/skill/grammar-card.md +1 -1
  104. package/src/agentsmd.ts +1 -1
  105. package/src/alias.ts +112 -0
  106. package/src/aontu.ts +20 -2
  107. package/src/cli.ts +630 -23
  108. package/src/ctx.ts +13 -0
  109. package/src/escape.ts +371 -0
  110. package/src/format.ts +648 -56
  111. package/src/hints.ts +94 -9
  112. package/src/lang.ts +430 -63
  113. package/src/lower.ts +636 -0
  114. package/src/lsp.ts +4 -4
  115. package/src/mcp-server.ts +3 -2
  116. package/src/mcp.ts +43 -4
  117. package/src/mod-tool.ts +9 -8
  118. package/src/mod.ts +6 -6
  119. package/src/render.ts +727 -0
  120. package/src/sigdecl.ts +1 -1
  121. package/src/std.ts +506 -1
  122. package/src/template.ts +291 -0
  123. package/src/unify.ts +47 -0
  124. package/src/val/AggFuncVal.ts +10 -21
  125. package/src/val/BagVal.ts +1 -1
  126. package/src/val/ConstraintVal.ts +1 -1
  127. package/src/val/EachFuncVal.ts +9 -19
  128. package/src/val/EmitFuncVal.ts +738 -0
  129. package/src/val/FilterFuncVal.ts +12 -7
  130. package/src/val/FormFuncVal.ts +119 -0
  131. package/src/val/FuncBaseVal.ts +18 -0
  132. package/src/val/MapVal.ts +3 -2
  133. package/src/val/PackFuncVal.ts +24 -22
  134. package/src/val/PlaceVal.ts +9 -6
  135. package/src/val/RefVal.ts +167 -18
  136. package/src/val/StrFuncVal.ts +334 -0
  137. package/src/val/Val.ts +42 -1
  138. package/src/val/members.ts +86 -0
@@ -0,0 +1,738 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // TRANSFORMATION: `emit(select, table)` (G9 phase 6,
4
+ // docs/design/EMIT.0.md). Apply-templates, with the dispatch in the
5
+ // engine and none of it in user space.
6
+ //
7
+ // emit($.services, [
8
+ // {match: {kind: sqs}, body: [`listen(` + .pin + `)`]}
9
+ // {match: {kind: http}, body: [`serve(` + .path + `)`]}
10
+ // ])
11
+ //
12
+ // For every node of `select`, in order, the first template whose
13
+ // `match` the node unifies with is taken and its `body` instantiated
14
+ // AGAINST THAT NODE. The result is one flat list.
15
+ //
16
+ // WHY THIS CANNOT BE WRITTEN IN THE DOCUMENT (EMIT.0.md, "What it
17
+ // takes"). A rule table held as a value cannot be dispatched against:
18
+ // a body referenced by path resolves its relative references AT THE
19
+ // DEFINITION SITE, and the positional resolution that does exist is a
20
+ // dot COUNT that does not survive being consumed by a second dispatch.
21
+ // The builtin instantiates a body at the node it matched -- a NAMED
22
+ // origin -- which is the whole capability.
23
+ //
24
+ // THE BODY'S RELATIVE REFERENCES ARE BOUND TO THE NODE. `.pin` inside
25
+ // a body is the matched node's `pin`, and `_` is the node itself, as
26
+ // it is in every other generator. The binding is done HERE rather than
27
+ // left to path resolution: a relative path is a COUNT taken wherever
28
+ // the value comes to rest, and the nodes of a computed selection
29
+ // (`filter(...)`) come to rest nowhere -- there is no position for a
30
+ // count to be taken from. An ABSOLUTE reference is untouched and still
31
+ // reads the document root.
32
+ //
33
+ // The binding stops at a nested generator's own binding argument
34
+ // (boundArgStart): a rule table nested in a body is the INNER emit's
35
+ // to bind, so `.x` inside it is the inner node. What crosses the
36
+ // boundary is the nested call's SELECTOR, which is argument 0 and is
37
+ // bound here -- the selector is the channel.
38
+ //
39
+ // THE RESULT IS FLAT, and that is a constraint rather than a
40
+ // convenience: the fragment algebra is flat because the nested
41
+ // spelling refuses even a valid instance in both ports, so a dispatch
42
+ // returning a tree would undo that ruling. A body element that is
43
+ // itself a list SPLICES, which is what makes a nested emit compose.
44
+ //
45
+ // A VALUE REACHES THE BODY THROUGH `replace`, NOT A DELIMITER
46
+ // (docs/design/TEMPLATE.0.md D3, D4; RENDER.0.md P6). A template may
47
+ // carry a `replace` map whose key is an exact string the body already
48
+ // holds as ordinary target text and whose value is evaluated against
49
+ // the matched node, and an `esc` naming the convention every value is
50
+ // escaped by -- the C/JSON escape when absent, `none` the one opt-out.
51
+ // Three rules: a single left-to-right scan of a literal line taking
52
+ // the longest key at each position; a substituted value is never
53
+ // re-scanned, so no value can introduce a key; and a template's
54
+ // replacements touch its own literal text only, never a result spliced
55
+ // in from a nested dispatch. Two checks run on the template before any
56
+ // node: a key inside another key (replace_overlap) and a key the body
57
+ // does not hold (replace_unused). There is no hole syntax, and that is
58
+ // the point: any inline delimiter is somebody's syntax.
59
+ //
60
+ // NO MATCH IS AN ERROR (`emit_none`). XSLT's built-in rule copies an
61
+ // unhandled node's string value into the result, which for code output
62
+ // means model data landing silently in the middle of a source file.
63
+ // That is the single worst default in the prior art and it is refused.
64
+ // An EMPTY selection, by contrast, emits nothing -- which is the whole
65
+ // conditional mechanism, and why no `when` directive exists.
66
+ //
67
+ // A NAMED TABLE IS A PLACEHELD `emit` (`%wire = emit(_, T)`). A table
68
+ // written at a document position is DRIVEN there, so its bodies'
69
+ // relative references resolve against wherever it sits and miss;
70
+ // nothing in the language holds a value unevaluated at such a
71
+ // position, and what does hold one is a CALL's template argument.
72
+ // `emit(.listen, %wire)` follows the reference and reads the table out
73
+ // of the placeheld call; `.listen & %wire` fills the hole. Both are
74
+ // the same dispatch, and it is what lets a rule set name ITSELF.
75
+ //
76
+ // TERMINATION IS THE SELECTION'S. Unlike `pack` and `each`, this one
77
+ // recurses -- a nested model walked into nested output is the
78
+ // capability the rule layer exists to add -- so the bound is not "it
79
+ // cannot call itself" but "each dispatch descends into a finite bag
80
+ // that already exists, and a selection that empties emits nothing". A
81
+ // rule set that walks into itself WITHOUT descending is charged to the
82
+ // depth budget and refused as `unify_cycle`, like any other runaway
83
+ // descent.
84
+
85
+ import type {
86
+ Val,
87
+ ValSpec,
88
+ } from '../type'
89
+
90
+ import {
91
+ AontuContext,
92
+ } from '../ctx'
93
+
94
+ import { unite } from '../unify'
95
+ import { makeNilErr } from '../err'
96
+ import { isEscVariant, escapeText } from '../escape'
97
+ import { cmpCodePoint } from '../keyorder'
98
+ import { top } from './top'
99
+ import { ListVal } from './ListVal'
100
+ import { StringVal } from './StringVal'
101
+ import { FuncBaseVal, trialUnify } from './FuncBaseVal'
102
+ import { repathInstance } from './Val'
103
+ import type { EmitOrigin } from './Val'
104
+ import { boundArgStart, fillPlace, rebuild } from './PlaceVal'
105
+ import { plusText } from './PlusOpVal'
106
+ import { bagMembers } from './members'
107
+
108
+
109
+ // One entry of the rule table: the pattern to try, the body to
110
+ // instantiate, and -- TEMPLATE.0.md D3 and D4 -- the `replace` map and
111
+ // the `esc` convention its values take, with the literal spots of the
112
+ // body the replacements are written into. A table is a LIST of these;
113
+ // a single template map is that list of one, told apart by kind,
114
+ // exactly as match() tells its patterns apart.
115
+ type Template = {
116
+ match: Val,
117
+ body: Val,
118
+ replace?: Val,
119
+ esc: string,
120
+ lits: LitSpot[],
121
+ // The rule's index in its table, which with the table's own address
122
+ // is the address the trace names it by (RENDER.0.md P7).
123
+ idx: number,
124
+ }
125
+
126
+
127
+ // One literal string of a body: element `i`, and within a map element
128
+ // the `of` index or the `text` key, with the text itself.
129
+ type LitSpot = { i: number, of?: number, text?: boolean, s: string }
130
+
131
+
132
+ // What is wrong with a table, with the detail the message carries.
133
+ type Refusal = { code: string, details?: Record<string, string> }
134
+
135
+
136
+ // One replacement: the key, and the text it becomes at a node.
137
+ type Pair = [string, string]
138
+
139
+
140
+ function isRefusal(x: any): x is Refusal {
141
+ return 'string' === typeof x.code
142
+ }
143
+
144
+
145
+ // Read the table. A map is one template; a list is many. The shape is
146
+ // checked here rather than at the call, because a table is ordinary
147
+ // data and may be computed.
148
+ function tableTemplates(table: Val | undefined): Template[] | Refusal {
149
+ const t: any = table
150
+
151
+ // A NAMED TABLE IS A PLACEHELD `emit`, and its table is the table.
152
+ // A table written at a document position is DRIVEN there -- a body's
153
+ // relative references resolve against wherever it sits and miss --
154
+ // so the position that holds one unevaluated is the one position the
155
+ // language already never drives: a call's template argument.
156
+ // `%wire = emit(_, [ … ])` is that position with the selection left
157
+ // open, and it reads as what it is, an apply-templates waiting for
158
+ // its nodes: `emit(.listen, %wire)` passes them, `.listen & %wire`
159
+ // fills the hole, and both are the same dispatch.
160
+ if (true === t?.isEmitFunc) {
161
+ return tableTemplates(t.peg[1])
162
+ }
163
+
164
+ if (true === t?.isMap) {
165
+ const one = oneTemplate(t, 0)
166
+ return isRefusal(one) ? one : [one]
167
+ }
168
+
169
+ if (true === t?.isList) {
170
+ const out: Template[] = []
171
+ for (const el of t.peg as Val[]) {
172
+ const e: any = el
173
+ if (true !== e?.isMap) {
174
+ return { code: 'emit_template' }
175
+ }
176
+ const one = oneTemplate(e, out.length)
177
+ if (isRefusal(one)) {
178
+ return one
179
+ }
180
+ out.push(one)
181
+ }
182
+ return out
183
+ }
184
+
185
+ return { code: 'emit_table' }
186
+ }
187
+
188
+
189
+ // One rule. Both keys are required: a template with no pattern would
190
+ // match everything by accident, and one with no body would emit
191
+ // nothing while claiming a node. The two optional keys -- a `replace`
192
+ // map and an `esc` naming the convention its values are escaped by,
193
+ // `none` the one opt-out -- are the template's shape too, and D3's two
194
+ // static checks run here, on the template alone, before any node.
195
+ function oneTemplate(m: any, idx: number): Template | Refusal {
196
+ const match: Val = m.peg.match
197
+ const body: any = m.peg.body
198
+ if (null == match || null == body) {
199
+ return { code: 'emit_template' }
200
+ }
201
+ if (true !== body.isList) {
202
+ return { code: 'emit_body' }
203
+ }
204
+
205
+ const replace: any = m.peg.replace
206
+ if (null != replace && true !== replace.isMap) {
207
+ return { code: 'emit_template' }
208
+ }
209
+
210
+ const escv: any = m.peg.esc
211
+ let esc = ''
212
+ if (null != escv) {
213
+ const name = textOf(escv)
214
+ if (undefined === name || ('none' !== name && !isEscVariant(name))) {
215
+ return { code: 'esc_variant' }
216
+ }
217
+ esc = name
218
+ }
219
+
220
+ const lits = literalSpots(body.peg)
221
+ if (null != replace) {
222
+ const bad = checkReplace(Object.keys(replace.peg), lits)
223
+ if (undefined !== bad) {
224
+ return bad
225
+ }
226
+ }
227
+
228
+ return { match, body, replace, esc, lits, idx }
229
+ }
230
+
231
+
232
+ // The string a value carries, or undefined when it is not a string.
233
+ function textOf(v: any): string | undefined {
234
+ return true === v?.isScalar && 'string' === typeof v.peg ? v.peg : undefined
235
+ }
236
+
237
+
238
+ // The literal strings of a body -- a string element, and the strings
239
+ // written directly in a map element's `of` list or `text` -- which are
240
+ // the text the template wrote. A string an expression or a nested
241
+ // dispatch computes is not one: D3's third rule (a spliced result is
242
+ // finished) and its second (a substituted value is never re-scanned)
243
+ // both follow from substituting at these spots and nowhere else.
244
+ function literalSpots(elems: Val[]): LitSpot[] {
245
+ const out: LitSpot[] = []
246
+ elems.forEach((el: any, i: number) => {
247
+ const s = textOf(el)
248
+ if (undefined !== s) {
249
+ out.push({ i, s })
250
+ return
251
+ }
252
+ if (true !== el?.isMap) {
253
+ return
254
+ }
255
+ const of: any = el.peg.of
256
+ if (true === of?.isList) {
257
+ (of.peg as any[]).forEach((p: any, j: number) => {
258
+ const ps = textOf(p)
259
+ if (undefined !== ps) {
260
+ out.push({ i, of: j, s: ps })
261
+ }
262
+ })
263
+ }
264
+ const ts = textOf(el.peg.text)
265
+ if (undefined !== ts) {
266
+ out.push({ i, text: true, s: ts })
267
+ }
268
+ })
269
+ return out
270
+ }
271
+
272
+
273
+ // D3's two static checks, on the template alone and before any node:
274
+ // a key inside another is ambiguous whatever the order
275
+ // (replace_overlap), and a key no literal holds means the template
276
+ // drifted from its map (replace_unused). Keys are visited in code
277
+ // point order, so both ports name the same pair.
278
+ function checkReplace(keys: string[], lits: LitSpot[]): Refusal | undefined {
279
+ const sorted = [...keys].sort(cmpCodePoint)
280
+ for (const a of sorted) {
281
+ for (const b of sorted) {
282
+ if (a !== b && b.includes(a)) {
283
+ return { code: 'replace_overlap', details: { key: quoted(a), other: quoted(b) } }
284
+ }
285
+ }
286
+ }
287
+ for (const k of sorted) {
288
+ if ('' === k || !lits.some((l) => l.s.includes(k))) {
289
+ return { code: 'replace_unused', details: { key: quoted(k) } }
290
+ }
291
+ }
292
+ return undefined
293
+ }
294
+
295
+
296
+ // D3's first two rules as one scan: at each position the longest key
297
+ // that matches is taken and its value written out whole, and the scan
298
+ // moves past the KEY -- the value is never looked at again, so no
299
+ // value can introduce a key.
300
+ function substitute(text: string, pairs: Pair[]): string {
301
+ let out = ''
302
+ let i = 0
303
+ while (i < text.length) {
304
+ const hit = pairs.find((p) => text.startsWith(p[0], i))
305
+ if (undefined === hit) {
306
+ out += text[i]
307
+ i += 1
308
+ }
309
+ else {
310
+ out += hit[1]
311
+ i += hit[0].length
312
+ }
313
+ }
314
+ return out
315
+ }
316
+
317
+
318
+ // The instance with the template's literal text at element `i`
319
+ // rewritten through the pairs -- on the fresh instance, where the
320
+ // structure is exactly the template's, and before any binding, so a
321
+ // value written in is never scanned again and a spliced result is
322
+ // never touched.
323
+ function substituted(
324
+ inst: Val, i: number, lits: LitSpot[], pairs: Pair[], ctx: AontuContext
325
+ ): Val {
326
+ for (const l of lits) {
327
+ if (l.i !== i) {
328
+ continue
329
+ }
330
+ const sv = new StringVal({ peg: substitute(l.s, pairs) }, ctx)
331
+ if (undefined !== l.of) {
332
+ (inst as any).peg.of.peg[l.of] = sv
333
+ }
334
+ else if (true === l.text) {
335
+ (inst as any).peg.text = sv
336
+ }
337
+ else {
338
+ inst = sv
339
+ }
340
+ }
341
+ return inst
342
+ }
343
+
344
+
345
+ // A key as the message writes it, quoted so an empty key is visible.
346
+ function quoted(s: string): string {
347
+ return '"' + s + '"'
348
+ }
349
+
350
+
351
+ function unpref(v: any): any {
352
+ while (true === v.isPref) {
353
+ v = v.peg
354
+ }
355
+ return v
356
+ }
357
+
358
+
359
+ // The reference a body named that the node could not answer, kept by
360
+ // the walk so `resolve` can report the first one against the node it
361
+ // was tried on -- and a refusal a replacement raised.
362
+ type Fail = { ref?: string, code?: string, details?: Record<string, string> }
363
+
364
+
365
+ // Every relative reference in `v` replaced by the field of `node` it
366
+ // names. Answers `v` unchanged when it holds none, so a body with no
367
+ // substitutions is never needlessly rebuilt -- the identity test
368
+ // `fillPlace` already relies on.
369
+ function bindNode(v: any, node: Val, ctx: AontuContext, fail: Fail): Val {
370
+ if (true === v?.isRef && true !== v.absolute) {
371
+ const found = nodeField(v, node)
372
+ if (undefined === found) {
373
+ fail.ref = undefined === fail.ref ? v.canon : fail.ref
374
+ return v
375
+ }
376
+ const out = found.clone(ctx)
377
+ // A RELATIVE REFERENCE IS A READ TOO (RENDER.0.md P7), and the one
378
+ // read no reference resolution sees: the binding answers it here,
379
+ // from the matched node, rather than letting a path resolve at a
380
+ // position the body never occupies. Without this a nested rule set
381
+ // whose selection is `.handlers` reported its nodes at the address
382
+ // they came to rest, which is in the OUTPUT. A node carries an
383
+ // address only under an instrumented run, which is what makes the
384
+ // second test the whole guard.
385
+ if (null == (out as any).origin && null != (node as any).origin) {
386
+ ; (out as any).origin = (node as any).origin +
387
+ (v.peg as string[]).map((seg: string) => '.' + seg).join('')
388
+ }
389
+ return out
390
+ }
391
+
392
+ const peg: any = v?.peg
393
+ const bound = boundArgStart(v)
394
+
395
+ if (Array.isArray(peg)) {
396
+ let changed = false
397
+ const out = peg.map((c: any, cI: number) => {
398
+ if (true !== c?.isVal || bound <= cI) {
399
+ return c
400
+ }
401
+ const b = bindNode(c, node, ctx, fail)
402
+ changed = changed || b !== c
403
+ return b
404
+ })
405
+ return changed ? rebuild(v, out, ctx) : v
406
+ }
407
+
408
+ if (true === peg?.isVal) {
409
+ const b = bindNode(peg, node, ctx, fail)
410
+ return b === peg ? v : rebuild(v, b, ctx)
411
+ }
412
+
413
+ if (null != peg && 'object' === typeof peg) {
414
+ let changed = false
415
+ const out: Record<string, Val> = {}
416
+ for (const k of Object.keys(peg)) {
417
+ const c = peg[k]
418
+ // No isVal guard, for the reason fillPlace gives: a slot holding
419
+ // something that is not a Val answers itself, because the tests
420
+ // above -- is it a reference, has it a peg -- are both false for
421
+ // one.
422
+ const b = bindNode(c, node, ctx, fail)
423
+ changed = changed || b !== c
424
+ out[k] = b
425
+ }
426
+ return changed ? rebuild(v, out, ctx) : v
427
+ }
428
+
429
+ return v
430
+ }
431
+
432
+
433
+ // The field of `node` a reference names, or undefined when it names
434
+ // none. Only a chain of plain NAMES is a field: a parent step has no
435
+ // answer at a node that is an origin rather than a position, and a
436
+ // variable segment is not a name until something resolves it -- both
437
+ // are refused here rather than left to resolve somewhere else, which
438
+ // is the failure mode the binding exists to remove.
439
+ function nodeField(ref: any, node: Val): Val | undefined {
440
+ let cur: any = node
441
+ for (const seg of ref.peg as any[]) {
442
+ if ('string' !== typeof seg || '.' === seg) {
443
+ return undefined
444
+ }
445
+ const peg: any = cur?.peg
446
+ if (true !== cur?.isBag || null == peg) {
447
+ return undefined
448
+ }
449
+ cur = true === cur.isList ? peg[Number(seg)] : peg[seg]
450
+ if (true !== cur?.isVal) {
451
+ return undefined
452
+ }
453
+ }
454
+ return cur
455
+ }
456
+
457
+
458
+ // The address of one matched node: its own read address when it has
459
+ // one, else the SELECTION's read address and the node's key under it
460
+ // -- a selection is read once and walked, so its members carry no read
461
+ // of their own.
462
+ //
463
+ // A COMPUTED SELECTION HAS NO ADDRESS, AND THE TRACE SAYS SO: an empty
464
+ // node. `filter(...)` builds a bag no path in the document names, and
465
+ // the only other thing to report is where the bag came to REST -- a
466
+ // position inside a template instance, which is not in the document,
467
+ // and which the two ports number differently. Publishing that would
468
+ // have made the trace a parity break as well as a fiction. The rule
469
+ // address answers the same way: `<table>#<index>` for a table a
470
+ // reference reached, and `#<index>` alone for one written inline at the
471
+ // call site, which has no address of its own. `#` is in no path, so a
472
+ // rule's address can never be read as one.
473
+ function nodeAddr(sel: string | undefined, key: string, node: any): string {
474
+ if (null != node.origin) {
475
+ return node.origin
476
+ }
477
+ return undefined === sel ? '' : sel + '.' + key
478
+ }
479
+
480
+
481
+ // A body element that is itself a list splices, which is what makes a
482
+ // nested emit compose into one flat sequence.
483
+ function splice(v: Val, out: Val[]): void {
484
+ if (true === (v as any)?.isList) {
485
+ for (const el of (v as any).peg as Val[]) {
486
+ splice(el, out)
487
+ }
488
+ return
489
+ }
490
+ out.push(v)
491
+ }
492
+
493
+
494
+ class EmitFuncVal extends FuncBaseVal {
495
+ isEmitFunc = true
496
+
497
+ // THE STAGING RULE (G8 phase 0, see AontuContext.settle). The
498
+ // selection is not settled merely by being `done` once -- a sibling
499
+ // conjunct, an include or a spread can still merge nodes into it,
500
+ // and pieces emitted from the half-merged bag would be missing.
501
+ staged = true
502
+
503
+ constructor(
504
+ spec: ValSpec,
505
+ ctx?: AontuContext
506
+ ) {
507
+ super(spec, ctx)
508
+ }
509
+
510
+
511
+ funcname() {
512
+ return 'emit'
513
+ }
514
+
515
+
516
+ // NEITHER ARGUMENT IS DRIVEN BY THE BASE. The selection is driven by
517
+ // hand below; the TABLE is not driven at all, because a body is a
518
+ // template and driving it would resolve its references at the call
519
+ // site -- the one position a body is never used at.
520
+ prepare(_ctx: AontuContext, _args: Val[]) {
521
+ return null
522
+ }
523
+
524
+
525
+ unify(peer: Val, ctx: AontuContext): Val {
526
+ // ONE argument is driven: the selection. The table holds bodies,
527
+ // which are templates (see prepare above).
528
+ if (!this.stagedReady(peer, ctx, 1)) {
529
+ return this.residuate(peer, ctx)
530
+ }
531
+
532
+ return super.unify(peer, ctx)
533
+ }
534
+
535
+
536
+ resolve(ctx: AontuContext, args: Val[]) {
537
+ // THE MEMBERS WITH THEIR KEYS, read through the one helper every
538
+ // fold reads a bag by (./members.ts): source order for a list,
539
+ // sorted-key order for a map, a hidden child and an unfilled
540
+ // optional left out. The KEY is what the trace addresses a node by
541
+ // -- it is the node's key IN THE SELECTION, which is the only
542
+ // thing a walk of a computed bag knows about where a node sits.
543
+ const nodes = bagMembers(args?.[0], ctx)
544
+ if (undefined === nodes) {
545
+ return makeNilErr(ctx, 'emit_data', this)
546
+ }
547
+
548
+ // A NAMED TABLE IS REACHED BY REFERENCE, and the reference -- not
549
+ // the table -- is what is followed. Followed HERE rather than in
550
+ // the staged drive, which waits for a SETTLED target: a table is a
551
+ // template, a template holding a hole never settles, and waiting
552
+ // for one would mean the dispatch never fires.
553
+ let table: any = args?.[1]
554
+ if (true === table?.isRef) {
555
+ table = table.unify(top(), ctx)
556
+ }
557
+
558
+ const templates = tableTemplates(table)
559
+ if (isRefusal(templates)) {
560
+ return this.refuse(ctx, templates)
561
+ }
562
+
563
+ // THE TRACE'S TWO ADDRESSES (RENDER.0.md D11, P7), computed once
564
+ // per dispatch and only when the run is instrumented: the table's
565
+ // own, which every rule of it is numbered under, and the
566
+ // selection's, which every node of it is keyed under.
567
+ const rec = undefined !== ctx.reads
568
+ const tableAddr = rec ? ((table as any)?.origin ?? '') : ''
569
+ const selAddr = rec ? (args?.[0] as any)?.origin : undefined
570
+
571
+ const peg: Val[] = []
572
+
573
+ for (const member of nodes) {
574
+ const node = member.val
575
+ const tmpl = this.dispatch(ctx, node, templates)
576
+ if ('string' === typeof tmpl) {
577
+ return makeNilErr(ctx, 'emit_none', this, undefined, 'resolve', {
578
+ value: node.canon,
579
+ tried: tmpl,
580
+ })
581
+ }
582
+ const fail: Fail = {}
583
+ let mark: EmitOrigin | undefined = undefined
584
+ if (rec) {
585
+ // THE NODE KEEPS ITS ADDRESS (P7). A body passes the node on
586
+ // through `_`, and a nested rule set dispatching over it can
587
+ // then say where it came from -- otherwise the node arrives as
588
+ // an element of a list the body wrote, and the only address
589
+ // left is where that list came to rest.
590
+ const naddr = nodeAddr(selAddr, member.key, node)
591
+ if ('' !== naddr && null == (node as any).origin) {
592
+ ; (node as any).origin = naddr
593
+ }
594
+ mark = { node: naddr, rule: tableAddr + '#' + tmpl.idx }
595
+ }
596
+ this.instantiate(ctx, node, tmpl, peg, fail, mark)
597
+ if (undefined !== fail.ref) {
598
+ return makeNilErr(ctx, 'emit_ref', this, undefined, 'resolve', {
599
+ ref: fail.ref,
600
+ value: node.canon,
601
+ })
602
+ }
603
+ if (undefined !== fail.code) {
604
+ return this.refuse(ctx, { code: fail.code, details: fail.details })
605
+ }
606
+ }
607
+
608
+ // THE PIECES ARE PATHED WHERE THEY LAND, once the splicing has
609
+ // settled how many there are. A piece keeps no trace of the body
610
+ // it was written in: the body is a template, and a template's
611
+ // parse position is the one place it is never used.
612
+ for (let i = 0; i < peg.length; i++) {
613
+ repathInstance(peg[i], [...ctx.path, String(i)])
614
+ }
615
+
616
+ return new ListVal({ peg }, ctx)
617
+ }
618
+
619
+
620
+ // The located error for a refusal, with the message's details when
621
+ // the refusal carries them.
622
+ refuse(ctx: AontuContext, r: Refusal): Val {
623
+ return undefined === r.details ? makeNilErr(ctx, r.code, this)
624
+ : makeNilErr(ctx, r.code, this, undefined, 'resolve', r.details)
625
+ }
626
+
627
+
628
+ // First match wins, in table order, by unifiability -- the same
629
+ // question `match` and `filter` ask, answered the same way. Returns
630
+ // the patterns tried when nothing matched, for the located error.
631
+ dispatch(ctx: AontuContext, node: Val, templates: Template[]): Template | string {
632
+ const tried: string[] = []
633
+ for (const tmpl of templates) {
634
+ tried.push(tmpl.match.canon)
635
+ // The trial is against CLONES: `unite` refines a bag in place
636
+ // against a TOP peer, and a pattern that failed must be untouched
637
+ // for the next node.
638
+ if (undefined !== trialUnify(ctx, node.clone(ctx), tmpl.match.clone(ctx))) {
639
+ return tmpl
640
+ }
641
+ }
642
+ return tried.join(' ')
643
+ }
644
+
645
+
646
+ // The replacement pairs for one node: the template's `replace` map
647
+ // instantiated at the node -- bound, filled and driven as a body is
648
+ // -- each value as text by the one number-to-text rule (`plusText`,
649
+ // the rule `+` and `join` share), escaped by the template's
650
+ // convention unless that is `none`, and sorted longest key first so
651
+ // the scan takes the longest match at every position (D3's first
652
+ // rule). A value that is not text, or has not settled, is
653
+ // replace_value.
654
+ replacements(ctx: AontuContext, node: Val, tmpl: Template, fail: Fail): Pair[] | undefined {
655
+ if (undefined === tmpl.replace) {
656
+ return undefined
657
+ }
658
+ let inst: any = tmpl.replace.clone(ctx, { dup: true })
659
+ inst = fillPlace(bindNode(inst, node, ctx, fail), node, ctx)
660
+ if (!inst.done) {
661
+ inst = unite(ctx, inst, top(), 'emit')
662
+ }
663
+ const pairs: Pair[] = []
664
+ for (const key of Object.keys(inst.peg)) {
665
+ const v: any = unpref(inst.peg[key])
666
+ const text = plusText(v)
667
+ if (undefined === text) {
668
+ fail.code = 'replace_value'
669
+ fail.details = { key: quoted(key), value: String(v?.canon) }
670
+ return undefined
671
+ }
672
+ pairs.push([key, 'none' === tmpl.esc ? text : escapeText(text, tmpl.esc)])
673
+ }
674
+ pairs.sort((a, b) => b[0].length - a[0].length || cmpCodePoint(a[0], b[0]))
675
+ return pairs
676
+ }
677
+
678
+
679
+ // Instantiate one body at the node and SPLICE its pieces into the
680
+ // output. A full instance to the leaves (`dup`, ADR-005), because a
681
+ // bare clone shares the inner structure of any call in the body and
682
+ // the first node's resolution would answer for every node; the
683
+ // template's replacements written into the instance's literal text;
684
+ // then the two bindings, relative references and the hole, both to
685
+ // the node.
686
+ instantiate(ctx: AontuContext, node: Val, tmpl: Template,
687
+ out: Val[], fail: Fail, mark?: EmitOrigin): void {
688
+ const pairs = this.replacements(ctx, node, tmpl, fail)
689
+ if (undefined !== fail.code) {
690
+ return
691
+ }
692
+
693
+ const elems: Val[] = (tmpl.body as any).peg
694
+
695
+ for (let i = 0; i < elems.length; i++) {
696
+ const elctx = ctx.descend(String(out.length))
697
+ let inst = elems[i].clone(elctx, { dup: true })
698
+ if (undefined !== pairs) {
699
+ inst = substituted(inst, i, tmpl.lits, pairs, elctx)
700
+ }
701
+ let piece = fillPlace(bindNode(inst, node, elctx, fail), node, elctx)
702
+
703
+ // A NESTED DISPATCH IS DRIVEN HERE, not left for the next pass.
704
+ // Its selection is bound and the model has settled, so it has
705
+ // everything it needs -- and it must answer NOW, because what
706
+ // makes the result flat is splicing its pieces into this one.
707
+ // Left standing, a nested `emit` resolved a pass later, as a
708
+ // list INSIDE the list, and the fragment algebra is flat.
709
+ // Through `unite` rather than by hand: a rule set that walks
710
+ // into itself for ever is charged to the depth budget and
711
+ // refused as `unify_cycle`, like any other runaway descent.
712
+ if (!piece.done) {
713
+ piece = unite(elctx, piece, top(), 'emit')
714
+ }
715
+
716
+ // THE INNERMOST DISPATCH OWNS THE PIECE (P7). A body element
717
+ // that is a nested rule set has already stamped what it emitted,
718
+ // and those pieces are spliced into this result here: the rule
719
+ // that WROTE a line is the one the trace names, so a stamp is
720
+ // written only where there is none.
721
+ const at = out.length
722
+ splice(piece, out)
723
+ if (undefined !== mark) {
724
+ for (let k = at; k < out.length; k++) {
725
+ if (null == (out[k] as any).emitted) {
726
+ ; (out[k] as any).emitted = mark
727
+ }
728
+ }
729
+ }
730
+ }
731
+ }
732
+
733
+ } /* node:coverage ignore next 6 */
734
+
735
+
736
+ export {
737
+ EmitFuncVal,
738
+ }