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
package/dist/format.js CHANGED
@@ -16,10 +16,11 @@ exports.unifiedDiff = unifiedDiff;
16
16
  // trees: a formatter that cannot prove its output is the same document
17
17
  // refuses rather than return it.
18
18
  //
19
- // This is the syntactic tier only (P1): whitespace, commas, quotes,
20
- // bare keys, chains and pair elements, none of which changes the parse
21
- // tree. The lawful tier -- the repeat-the-prefix rewrite that rests on
22
- // the meet -- is P2, and lands behind its own local check.
19
+ // Two tiers. The syntactic (P1): whitespace, commas, quotes, bare
20
+ // keys, chains and pair elements, none of which changes the parse
21
+ // tree. The lawful (P2), over it: repeat the prefix, and merge what
22
+ // repeats -- rewrites that rest on the meet, each checked by the meet
23
+ // in isolation and kept only where the engine agrees.
23
24
  //
24
25
  // The Go twin is go/format.go, function for function; the shared
25
26
  // behaviour is test/spec/fmt.tsv, executed by both spec runners.
@@ -212,26 +213,33 @@ class Reader {
212
213
  opener = false;
213
214
  gap = false;
214
215
  }
216
+ // A blank line before the closer is no paragraph break: nothing
217
+ // follows it, and the layout would drop it anyway.
218
+ while (0 < body.length && 'blank' === body[body.length - 1].t) {
219
+ body.pop();
220
+ }
215
221
  return { body, open };
216
222
  }
217
223
  // One entry: an include, a spread, a pair, or -- as a list element or
218
224
  // at the root -- a value.
219
225
  entry() {
220
226
  const n = this.name(0);
227
+ const at = this.T[this.i].sI;
221
228
  if ('#OD_multisource' === n) {
222
229
  const text = '@' + normStr(this.T[this.i + 1].src);
223
230
  this.i += 2;
224
- return { t: 'include', text };
231
+ return { t: 'include', text, at };
225
232
  }
226
233
  if ('#E&' === n && '#CL' === this.name(1)) {
227
234
  this.i += 2;
228
- return { t: 'spread', value: this.value() };
235
+ return { t: 'spread', value: this.value(), at };
229
236
  }
230
237
  if (this.atKey()) {
231
238
  const tok = this.T[this.i];
232
239
  const opt = '#QM' === this.name(1);
240
+ const alias = '=' === this.T[this.i + (opt ? 2 : 1)].src;
233
241
  this.i += opt ? 3 : 2;
234
- return { t: 'pair', key: keyText(tok), opt, value: this.value() };
242
+ return { t: 'pair', key: keyText(tok), opt, alias, value: this.value(), at };
235
243
  }
236
244
  return this.value();
237
245
  }
@@ -258,12 +266,13 @@ class Reader {
258
266
  if (!this.open(items) && !BINARY[n] && '#LN' !== n && '#CM' !== n) {
259
267
  break;
260
268
  }
269
+ const at = this.T[this.i].sI;
261
270
  if ('#E&' === n && '#CL' === this.name(1)) {
262
271
  if (0 === items.length) {
263
272
  // A chain through a spread, `a: &: integer`. The braces are
264
273
  // the agreed spelling (X-7), so it is read as the map it is.
265
274
  this.i += 2;
266
- return { t: 'map', body: [{ t: 'spread', value: this.value() }] };
275
+ return { t: 'map', body: [{ t: 'spread', value: this.value(), at }], at };
267
276
  }
268
277
  // A sibling spread in a list, `[1 &: 2]`: this value is complete.
269
278
  break;
@@ -283,7 +292,7 @@ class Reader {
283
292
  // operator, or on a line the value continues past. Otherwise
284
293
  // it trails the statement and the caller attaches it.
285
294
  if (this.open(items) || BINARY[this.name(this.significant())]) {
286
- items.push({ t: 'note', text: this.T[this.i].src });
295
+ items.push({ t: 'note', text: this.T[this.i].src, at });
287
296
  this.i++;
288
297
  continue;
289
298
  }
@@ -292,13 +301,13 @@ class Reader {
292
301
  if (BINARY[n]) {
293
302
  items.push({
294
303
  t: 'op', text: this.T[this.i].src,
295
- brk: '#LN' === this.name(-1) || '#LN' === this.name(1),
304
+ brk: '#LN' === this.name(-1) || '#LN' === this.name(1), at,
296
305
  });
297
306
  this.i++;
298
307
  continue;
299
308
  }
300
309
  if (PREFIX[n]) {
301
- items.push({ t: 'prefix', text: this.T[this.i].src });
310
+ items.push({ t: 'prefix', text: this.T[this.i].src, at });
302
311
  this.i++;
303
312
  continue;
304
313
  }
@@ -306,7 +315,7 @@ class Reader {
306
315
  this.i++;
307
316
  const inner = this.seq();
308
317
  this.i++;
309
- items.push({ t: 'paren', inner });
318
+ items.push({ t: 'paren', inner, at });
310
319
  continue;
311
320
  }
312
321
  if ('#TX' === n && '#E(' === this.name(1)) {
@@ -314,25 +323,25 @@ class Reader {
314
323
  this.i += 2;
315
324
  const args = this.seq();
316
325
  this.i++;
317
- items.push({ t: 'call', name, args });
326
+ items.push({ t: 'call', name, args, at });
318
327
  continue;
319
328
  }
320
329
  if ('#OB' === n) {
321
330
  this.i++;
322
331
  const m = this.body('#CB', true);
323
332
  this.i++;
324
- items.push({ t: 'map', body: m.body, open: m.open });
333
+ items.push({ t: 'map', body: m.body, open: m.open, at });
325
334
  continue;
326
335
  }
327
336
  if ('#OS' === n) {
328
337
  this.i++;
329
338
  const l = this.body('#CS', true);
330
339
  this.i++;
331
- items.push({ t: 'list', body: l.body, open: l.open });
340
+ items.push({ t: 'list', body: l.body, open: l.open, at });
332
341
  continue;
333
342
  }
334
343
  if ('#OD_multisource' === n) {
335
- items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src) });
344
+ items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src), at });
336
345
  this.i += 2;
337
346
  continue;
338
347
  }
@@ -348,7 +357,8 @@ class Reader {
348
357
  'note' !== items[0].t) {
349
358
  return items[0];
350
359
  }
351
- return { t: 'expr', items };
360
+ // An empty value, `a:`, is an expression with nothing in it.
361
+ return { t: 'expr', items, at: items[0]?.at };
352
362
  }
353
363
  // Whether the expression so far wants an operand: nothing yet, or an
354
364
  // operator, a prefix or a comment last.
@@ -361,6 +371,7 @@ class Reader {
361
371
  }
362
372
  // The token under the cursor, and the parts glued to it.
363
373
  atom() {
374
+ const at = this.T[this.i].sI;
364
375
  let text = atomText(this.T[this.i]);
365
376
  this.i++;
366
377
  while (GLUE[this.name(0)] &&
@@ -368,7 +379,7 @@ class Reader {
368
379
  text += atomText(this.T[this.i]);
369
380
  this.i++;
370
381
  }
371
- return { t: 'atom', text };
382
+ return { t: 'atom', text, at };
372
383
  }
373
384
  // A call's arguments, or a parenthesis's contents, up to the closing
374
385
  // parenthesis: values separated by commas, with a comment among them
@@ -376,6 +387,7 @@ class Reader {
376
387
  seq() {
377
388
  const out = [];
378
389
  let gap = true;
390
+ let comma = false;
379
391
  for (;;) {
380
392
  const n = this.name(0);
381
393
  if ('' === n || CLOSER[n]) {
@@ -387,9 +399,10 @@ class Reader {
387
399
  }
388
400
  if ('#CA' === n) {
389
401
  if (gap) {
390
- out.push({ t: 'atom', text: 'nil' });
402
+ out.push({ t: 'atom', text: 'nil', sep: comma });
391
403
  }
392
404
  gap = true;
405
+ comma = true;
393
406
  this.i++;
394
407
  continue;
395
408
  }
@@ -398,8 +411,11 @@ class Reader {
398
411
  this.i++;
399
412
  continue;
400
413
  }
401
- out.push(this.value());
414
+ const v = this.value();
415
+ v.sep = comma;
416
+ out.push(v);
402
417
  gap = false;
418
+ comma = false;
403
419
  }
404
420
  return out;
405
421
  }
@@ -451,6 +467,11 @@ function width(s) {
451
467
  return Array.from(s).length;
452
468
  }
453
469
  function pairHead(node, tight) {
470
+ // An alias declaration is `%name = value` at every width: the `=` is
471
+ // an operator, and operators are spaced (§3.2).
472
+ if (node.alias) {
473
+ return node.key + ' = ';
474
+ }
454
475
  return node.key + (node.opt ? '?' : '') + (tight ? ':' : ': ');
455
476
  }
456
477
  // The one-line spelling of a node, or undefined where it has none: a
@@ -508,16 +529,22 @@ function inline(node, tight) {
508
529
  return undefined;
509
530
  }
510
531
  }
532
+ // Arguments on one line, each after the separator the author wrote
533
+ // (§3.6): a comma stays a comma, and a space a space, because the
534
+ // parser reads `must((v) => 0 <= v, "…")` as a run of arguments too.
511
535
  function inlineSeq(items) {
512
- const parts = [];
513
- for (const it of items) {
514
- const s = inline(it, true);
536
+ let out = '';
537
+ for (let k = 0; k < items.length; k++) {
538
+ const s = inline(items[k], true);
515
539
  if (undefined === s) {
516
540
  return undefined;
517
541
  }
518
- parts.push(s);
542
+ out += (0 === k ? '' : sepOf(items[k])) + s;
519
543
  }
520
- return parts.join(', ');
544
+ return out;
545
+ }
546
+ function sepOf(node) {
547
+ return node.sep ? ', ' : ' ';
521
548
  }
522
549
  // Binary operators spaced, prefixes tight (§3.11). An operand is
523
550
  // never directly after an operand: the reader ends a value there.
@@ -570,6 +597,23 @@ class Writer {
570
597
  width() {
571
598
  return width(this.line);
572
599
  }
600
+ // Where the page is, and the lines written since, the current line
601
+ // included: the spelling of one statement, as it stands on the page.
602
+ mark() {
603
+ return this.lines.length;
604
+ }
605
+ since(mark) {
606
+ return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n';
607
+ }
608
+ // The lines since a mark replaced by a text: the spelling before,
609
+ // where a rewrite did not pass its check.
610
+ replace(mark, text) {
611
+ const lines = text.split('\n');
612
+ lines.pop();
613
+ this.line = lines.pop();
614
+ this.lines.length = mark;
615
+ this.lines.push(...lines);
616
+ }
573
617
  finish() {
574
618
  if (!this.started) {
575
619
  return '';
@@ -585,8 +629,11 @@ function rtrim(s) {
585
629
  }
586
630
  // The entries of a body, one per line at the indentation, with the
587
631
  // blank lines the author kept between them (§3.8) -- never at the
588
- // start or the end.
589
- function emitBody(w, body, indent) {
632
+ // start or the end. In STATEMENT position (`stmt`: the root, and the
633
+ // body of a plain map that is itself the value of a statement) a pair
634
+ // is laid out by §3.4, which may repeat its key; anywhere else -- a
635
+ // list, an operand, an argument -- by §3.5 alone.
636
+ function emitBody(w, body, indent, stmt) {
590
637
  let pending = false;
591
638
  let count = 0;
592
639
  for (const node of body) {
@@ -601,6 +648,10 @@ function emitBody(w, body, indent) {
601
648
  w.text(node.text);
602
649
  continue;
603
650
  }
651
+ if (undefined !== stmt && 'pair' === node.t) {
652
+ emitStatement(w, node, indent, stmt, '');
653
+ continue;
654
+ }
604
655
  const e = chain(node);
605
656
  emitValue(w, e, indent);
606
657
  if (undefined !== e.trail) {
@@ -632,10 +683,10 @@ function emitValue(w, node, indent) {
632
683
  emitValue(w, node.value, indent);
633
684
  return;
634
685
  case 'map':
635
- emitBlock(w, '{', '}', node, indent);
686
+ emitBlock(w, '{', '}', node, indent, undefined);
636
687
  return;
637
688
  case 'list':
638
- emitBlock(w, '[', ']', node, indent);
689
+ emitBlock(w, '[', ']', node, indent, undefined);
639
690
  return;
640
691
  case 'expr':
641
692
  emitExpr(w, node.items, indent);
@@ -649,27 +700,36 @@ function emitValue(w, node, indent) {
649
700
  }
650
701
  }
651
702
  // A call, or a parenthesis, that has no one-line form or is too wide
652
- // for the budget. Three shapes. A single container argument hugs the
653
- // parentheses, `close({` ... `})`, and decides its own lines. Arguments
654
- // that each have a one-line form stay on the one line however wide it
655
- // is: the formatter never breaks a line. Otherwise -- an argument that
656
- // is itself several lines, a comment among the arguments -- the
657
- // parenthesis opens a block: one argument per line one level in, the
658
- // closer alone at the opener's level.
703
+ // for the budget. Three shapes. Arguments that are all FLAT -- none
704
+ // holds a container -- stay on the one line however wide it is: a
705
+ // scalar is no narrower on a line of its own, and the formatter never
706
+ // breaks a line. The last argument HUGS the parentheses, `hide({` ...
707
+ // `})`, `close($.E & {` ... `})`, when it is a container, or an
708
+ // expression the author did not break that ends in one, and the
709
+ // arguments before it fit on the opener's line: the container decides
710
+ // its own lines. Otherwise the parenthesis opens a block: one argument
711
+ // per line one level in, the closer alone at the opener's level. A
712
+ // call whose last argument hugs is hugged in turn, `type(close({` ...
713
+ // `}))`: the schema idiom.
659
714
  function emitCall(w, node, indent) {
660
715
  const items = 'call' === node.t ? node.args : node.inner;
661
716
  const open = ('call' === node.t ? node.name : '') + '(';
662
- if (1 === items.length && ('map' === items[0].t || 'list' === items[0].t)) {
663
- w.text(open);
664
- emitValue(w, items[0], indent);
665
- w.text(')');
666
- return;
667
- }
668
717
  const one = inlineSeq(items);
669
- if (undefined !== one) {
718
+ if (undefined !== one && !items.some(holdsContainer)) {
670
719
  w.text(open + one + ')');
671
720
  return;
672
721
  }
722
+ const last = items[items.length - 1];
723
+ if (0 < items.length && hugs(last)) {
724
+ const head = inlineSeq(items.slice(0, -1));
725
+ const lead = '' === head ? '' : head + sepOf(last);
726
+ if (undefined !== head && ('' === head || w.width() + width(open + lead) <= BUDGET)) {
727
+ w.text(open + lead);
728
+ emitValue(w, last, indent);
729
+ w.text(')');
730
+ return;
731
+ }
732
+ }
673
733
  w.text(open);
674
734
  let noted = false;
675
735
  for (let k = 0; k < items.length; k++) {
@@ -690,7 +750,8 @@ function emitCall(w, node, indent) {
690
750
  }
691
751
  w.open(indent + 2, false);
692
752
  emitValue(w, it, indent + 2);
693
- if (items.slice(k + 1).some((x) => 'note' !== x.t)) {
753
+ const next = items.slice(k + 1).find((x) => 'note' !== x.t);
754
+ if (undefined !== next && next.sep) {
694
755
  w.text(',');
695
756
  }
696
757
  noted = false;
@@ -698,10 +759,41 @@ function emitCall(w, node, indent) {
698
759
  w.open(indent, false);
699
760
  w.text(')');
700
761
  }
762
+ // Whether a node holds a container anywhere: the argument has a
763
+ // several-line form of its own.
764
+ function holdsContainer(node) {
765
+ switch (node.t) {
766
+ case 'map':
767
+ case 'list':
768
+ return true;
769
+ case 'call':
770
+ return node.args.some(holdsContainer);
771
+ case 'paren':
772
+ return node.inner.some(holdsContainer);
773
+ case 'expr':
774
+ return node.items.some(holdsContainer);
775
+ default:
776
+ return false;
777
+ }
778
+ }
779
+ // Whether a last argument hugs the parentheses: a container; an
780
+ // expression with no break and no comment whose last operand is one;
781
+ // a call whose own last argument does.
782
+ function hugs(node) {
783
+ if ('map' === node.t || 'list' === node.t) {
784
+ return true;
785
+ }
786
+ if ('call' === node.t) {
787
+ return 0 < node.args.length && hugs(node.args[node.args.length - 1]);
788
+ }
789
+ return 'expr' === node.t &&
790
+ node.items.every((it) => 'note' !== it.t && !('op' === it.t && it.brk)) &&
791
+ hugs(node.items[node.items.length - 1]);
792
+ }
701
793
  // A container on several lines (§3.5): the opener ends its line, the
702
794
  // entries are statements one level in, the closer stands alone. An
703
795
  // empty container is inline whatever the budget says.
704
- function emitBlock(w, open, close, node, indent) {
796
+ function emitBlock(w, open, close, node, indent, stmt) {
705
797
  if (0 === node.body.length && undefined === node.open) {
706
798
  w.text(open + close);
707
799
  return;
@@ -710,7 +802,7 @@ function emitBlock(w, open, close, node, indent) {
710
802
  if (undefined !== node.open) {
711
803
  w.text(' ' + node.open);
712
804
  }
713
- emitBody(w, node.body, indent + 2);
805
+ emitBody(w, node.body, indent + 2, stmt);
714
806
  w.open(indent, false);
715
807
  w.text(close);
716
808
  }
@@ -765,23 +857,420 @@ function emitExpr(w, items, indent) {
765
857
  operand = true;
766
858
  }
767
859
  }
768
- function emit(root) {
860
+ // The entries of a plain map value: a braced map, or a chain, which is
861
+ // a one-entry map. A map with a comment on its opener keeps its braces
862
+ // (§3.7), so it is not plain here; nor is a map holding an include,
863
+ // which the local check cannot follow.
864
+ function plainEntries(v) {
865
+ if ('pair' === v.t) {
866
+ return [v];
867
+ }
868
+ if ('map' !== v.t || undefined !== v.open || v.body.some((e) => 'include' === e.t)) {
869
+ return undefined;
870
+ }
871
+ return v.body;
872
+ }
873
+ // The entries of a statement as they stand once it is merged into a
874
+ // wider map: its trailing comment sunk onto its last entry, so that it
875
+ // travels with the entry it stood beside. Undefined where the value is
876
+ // not a plain map, or the comment has no entry to sit on.
877
+ function members(p) {
878
+ const entries = plainEntries(p.value);
879
+ if (undefined === entries || undefined === p.trail) {
880
+ return entries;
881
+ }
882
+ const last = entries[entries.length - 1];
883
+ if (undefined === last || ('pair' !== last.t && 'spread' !== last.t)) {
884
+ return undefined;
885
+ }
886
+ const trail = undefined === last.trail ? p.trail : last.trail + ' ' + p.trail;
887
+ return entries.slice(0, -1).concat([{ ...last, trail }]);
888
+ }
889
+ // Adjacent statements naming one key, whose values are plain maps, are
890
+ // one map: their entries in order, with the comments and blank lines
891
+ // between the statements travelling with the statement they preceded.
892
+ // Only ADJACENT statements merge -- a `server:` line, something else,
893
+ // then another `server:` line stays as it is, because merging them
894
+ // would move a statement, and the formatter never reorders (§3.13).
895
+ // Nor do two statements merge into a map with two spreads: the engine
896
+ // keeps those as a conjunction, which is not the meet of the two maps.
897
+ // The tree is not changed: a merged statement is a new node that keeps
898
+ // the statements it replaces as its `orig`, its spelling before, and a
899
+ // statement merged somewhere below is copied the same way.
900
+ function mergeRuns(body) {
901
+ const out = [];
902
+ let i = 0;
903
+ while (i < body.length) {
904
+ const first = body[i];
905
+ const entries = 'pair' === first.t ? members(first) : undefined;
906
+ if (undefined === entries) {
907
+ out.push('pair' === first.t ? mergeDeep(first) : first);
908
+ i++;
909
+ continue;
910
+ }
911
+ const group = [first];
912
+ let merged = entries;
913
+ let carry = [];
914
+ let j = i + 1;
915
+ for (; j < body.length; j++) {
916
+ const n = body[j];
917
+ if ('comment' === n.t || 'blank' === n.t) {
918
+ carry.push(n);
919
+ continue;
920
+ }
921
+ const more = 'pair' === n.t && n.key === first.key && n.opt === first.opt
922
+ ? members(n) : undefined;
923
+ if (undefined === more || (spreads(merged) && spreads(more))) {
924
+ break;
925
+ }
926
+ group.push(...carry, n);
927
+ merged = merged.concat(carry, more);
928
+ carry = [];
929
+ }
930
+ if (1 === group.length) {
931
+ out.push(mergeDeep(first));
932
+ i++;
933
+ continue;
934
+ }
935
+ out.push({
936
+ t: 'pair', key: first.key, opt: first.opt, alias: first.alias,
937
+ value: { t: 'map', body: mergeRuns(merged) }, orig: group,
938
+ });
939
+ i = j - carry.length;
940
+ }
941
+ return out;
942
+ }
943
+ function spreads(entries) {
944
+ return entries.some((e) => 'spread' === e.t);
945
+ }
946
+ // The merge down a statement's plain-map spine: a chain's inner pair,
947
+ // or the entries of a map value, are statements of the map they are
948
+ // in. The statement itself where nothing below it merged.
949
+ function mergeDeep(p) {
950
+ const v = p.value;
951
+ const entries = plainEntries(v);
952
+ if (undefined === entries) {
953
+ return p;
954
+ }
955
+ const body = mergeRuns(entries);
956
+ if (body.length === entries.length && body.every((n, k) => n === entries[k])) {
957
+ return p;
958
+ }
959
+ return { ...p, value: 'pair' === v.t ? body[0] : { ...v, body }, orig: [p] };
960
+ }
961
+ function repeatLines(entries, prefix, indent) {
962
+ if (0 === entries.length || 'comment' === entries[entries.length - 1].t ||
963
+ 1 < entries.filter((e) => 'spread' === e.t).length) {
964
+ return undefined;
965
+ }
966
+ const out = [];
967
+ for (const e of entries) {
968
+ if ('blank' === e.t) {
969
+ out.push({ t: 'blank' });
970
+ continue;
971
+ }
972
+ if ('comment' === e.t) {
973
+ out.push({ t: 'comment', text: e.text });
974
+ continue;
975
+ }
976
+ const trail = undefined === e.trail ? '' : ' ' + e.trail;
977
+ if ('spread' === e.t) {
978
+ // The repeated spread entry is a one-entry map holding only a
979
+ // spread, so by D1's exception it keeps its braces.
980
+ const s = inline(e.value, true);
981
+ if (undefined === s || !fits(indent, prefix + '{ &: ' + s + ' }')) {
982
+ return undefined;
983
+ }
984
+ out.push({ t: 'text', text: prefix + '{ &: ' + s + ' }' + trail });
985
+ continue;
986
+ }
987
+ const head = prefix + pairHead(e, false);
988
+ const s = inline(chain(e.value), false);
989
+ if (undefined !== s && fits(indent, head + s)) {
990
+ out.push({ t: 'text', text: head + s + trail });
991
+ continue;
992
+ }
993
+ const sub = plainEntries(e.value);
994
+ if (undefined === sub) {
995
+ return undefined;
996
+ }
997
+ const lines = repeatLines(sub, head, indent);
998
+ if (undefined === lines) {
999
+ return undefined;
1000
+ }
1001
+ if ('' !== trail) {
1002
+ lines[lines.length - 1].text += trail;
1003
+ }
1004
+ out.push(...lines);
1005
+ }
1006
+ return out;
1007
+ }
1008
+ function fits(indent, text) {
1009
+ return indent + width(text) <= BUDGET;
1010
+ }
1011
+ // A pair in statement position, by §3.4. `prefix` is what stands
1012
+ // before it on its line: the heads of the chain it hangs from, not yet
1013
+ // written. Its value is laid out by §3.5 unless it is a plain map, and
1014
+ // then in this order: a chain, when the map holds exactly one pair
1015
+ // (D1); one line, when that fits the budget; the key repeated over the
1016
+ // entries, when every entry can be one line that way; a braced block
1017
+ // otherwise, whose entries are statements in turn. Whether the
1018
+ // statement was rewritten by this tier -- merged, or repeated -- is
1019
+ // returned, and the outermost such statement is checked: its spelling
1020
+ // on the page against what the syntactic tier writes for the
1021
+ // statements it came from, at the same indentation, which is what
1022
+ // stays on the page when the check fails.
1023
+ function emitStatement(w, p, indent, stmt, prefix) {
1024
+ const mark = w.mark();
1025
+ let rewritten = undefined !== p.orig;
1026
+ const entries = plainEntries(p.value);
1027
+ const head = prefix + pairHead(p, false);
1028
+ const s = undefined === entries ? undefined : inline(p.value, false);
1029
+ if (undefined === entries) {
1030
+ w.text(prefix);
1031
+ emitValue(w, p, indent);
1032
+ }
1033
+ else if (1 === entries.length && 'pair' === entries[0].t) {
1034
+ rewritten = emitStatement(w, entries[0], indent, { meet: stmt.meet, covered: true }, head)
1035
+ || rewritten;
1036
+ }
1037
+ else if (undefined !== s && fits(indent, head + s)) {
1038
+ w.text(head + s);
1039
+ }
1040
+ else {
1041
+ const lines = repeatLines(entries, head, indent);
1042
+ if (undefined !== lines) {
1043
+ let pending = false;
1044
+ let count = 0;
1045
+ for (const line of lines) {
1046
+ if ('blank' === line.t) {
1047
+ pending = 0 < count;
1048
+ continue;
1049
+ }
1050
+ if (0 < count) {
1051
+ w.open(indent, pending);
1052
+ }
1053
+ pending = false;
1054
+ count++;
1055
+ w.text(line.text);
1056
+ }
1057
+ rewritten = true;
1058
+ }
1059
+ else {
1060
+ w.text(head);
1061
+ emitBlock(w, '{', '}', p.value, indent, { meet: stmt.meet, covered: stmt.covered || rewritten });
1062
+ }
1063
+ }
1064
+ if (undefined !== p.trail) {
1065
+ w.text(' ' + p.trail);
1066
+ }
1067
+ if (rewritten && !stmt.covered) {
1068
+ const before = emitAt(p.orig ?? [p], indent);
1069
+ if (!stmt.meet(before, w.since(mark))) {
1070
+ w.replace(mark, before);
1071
+ }
1072
+ }
1073
+ return rewritten;
1074
+ }
1075
+ // The syntactic tier's spelling of some statements at an indentation:
1076
+ // a rewrite's spelling before.
1077
+ function emitAt(nodes, indent) {
1078
+ const w = new Writer();
1079
+ emitBody(w, nodes, indent, undefined);
1080
+ return w.finish();
1081
+ }
1082
+ // The document: by the syntactic tier alone, or with the lawful tier
1083
+ // over it when given its check.
1084
+ function emit(root, meet) {
769
1085
  const w = new Writer();
770
- emitBody(w, root, 0);
1086
+ emitBody(w, undefined === meet ? root : mergeRuns(root), 0, undefined === meet ? undefined : { meet, covered: false });
771
1087
  return w.finish();
772
1088
  }
773
1089
  // ---------------------------------------------------------------------
1090
+ // The lint (§4): what the formatter points at and never touches. Two
1091
+ // rules, both advice: the formatter never renames a key (§4.1) and
1092
+ // never introduces an alias (§4.2), and a rule with a mechanical fix
1093
+ // that keeps the document would belong to §3 instead (§4.3).
1094
+ // The shape width at which a repeat is worth an alias (§4.2): below
1095
+ // it, `{ a:1 }` twice is the shorter spelling. Measured over the use
1096
+ // cases when the lint landed (§7.10).
1097
+ const REPEAT_MIN_WIDTH = 40;
1098
+ function lintOf(root, text) {
1099
+ const out = [];
1100
+ const nodes = root.map(lintNode);
1101
+ for (const n of nodes) {
1102
+ keyCase(n, text, out);
1103
+ }
1104
+ repeats(nodes, text, out);
1105
+ out.sort((a, b) => a.line - b.line || a.col - b.col);
1106
+ return out;
1107
+ }
1108
+ // The tree the lint walks: a chain's inner pair as the one-entry map
1109
+ // it is, so that `a: {b: 1}` and `a: b: 1` -- one document to the
1110
+ // formatter -- are one shape to the lint.
1111
+ function lintNode(node) {
1112
+ if ('pair' === node.t && 'pair' === node.value.t) {
1113
+ return { ...node, value: { t: 'map', body: [node.value], at: node.value.at } };
1114
+ }
1115
+ return node;
1116
+ }
1117
+ function lintChildren(node) {
1118
+ switch (node.t) {
1119
+ case 'pair':
1120
+ case 'spread':
1121
+ return [lintNode(node).value];
1122
+ case 'map':
1123
+ case 'list':
1124
+ return node.body.map(lintNode);
1125
+ case 'call':
1126
+ return node.args;
1127
+ case 'paren':
1128
+ return node.inner;
1129
+ case 'expr':
1130
+ return node.items;
1131
+ default:
1132
+ return [];
1133
+ }
1134
+ }
1135
+ // Line and column, 1-based, of a source index.
1136
+ function lineCol(text, at) {
1137
+ const before = text.slice(0, at);
1138
+ return { line: before.split('\n').length, col: at - before.lastIndexOf('\n') };
1139
+ }
1140
+ // D4 (§4.1): keys are lower-case words, or CamelCase when a key is
1141
+ // several. A bare key holding `_`, or beginning with two capitals, is
1142
+ // reported with the spelling that would follow the form; a quoted key
1143
+ // is a deliberate spelling and a key of underscores alone names
1144
+ // nothing the rule can respell.
1145
+ function keyCase(node, text, out) {
1146
+ if ('pair' === node.t && BARE.test(node.key) && /[A-Za-z]/.test(node.key)) {
1147
+ const why = node.key.includes('_') ? 'holds an underscore'
1148
+ : /^[A-Z][A-Z]/.test(node.key) ? 'begins with capitals' : '';
1149
+ if ('' !== why) {
1150
+ out.push({
1151
+ rule: 'style/key-case', ...lineCol(text, node.at),
1152
+ message: `key ${node.key} ${why}; ${camel(node.key)} would follow the form`,
1153
+ });
1154
+ }
1155
+ }
1156
+ for (const child of lintChildren(node)) {
1157
+ keyCase(child, text, out);
1158
+ }
1159
+ }
1160
+ // The key as lower-case words or CamelCase: `credit_cents` is
1161
+ // `creditCents`, `HTTP_PORT` is `httpPort`, `HTTPServer` is
1162
+ // `httpServer`, `ID` is `id`.
1163
+ function camel(key) {
1164
+ const words = key.split('_').filter((w) => '' !== w)
1165
+ .map((w) => /^[A-Z]+$/.test(w) ? w.toLowerCase() : w);
1166
+ const head = words[0].replace(/^[A-Z]+(?=[A-Z][a-z])/, (run) => run.toLowerCase());
1167
+ return head.charAt(0).toLowerCase() + head.slice(1) +
1168
+ words.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
1169
+ }
1170
+ // D3 (§4.2): a shape written twice can drift, and an alias names it
1171
+ // once. Every map or list whose shape recurs in the file, and whose
1172
+ // shape is REPEAT_MIN_WIDTH or wider, is reported once, at its first
1173
+ // site, with the count and the other sites; the naming is the
1174
+ // author's. A repeat inside a repeat is the outer one's: the walk does
1175
+ // not descend into a shape it reports.
1176
+ function repeats(nodes, text, out) {
1177
+ const counts = new Map();
1178
+ const tally = (node) => {
1179
+ if ('map' === node.t || 'list' === node.t) {
1180
+ const s = shape(node);
1181
+ counts.set(s, (counts.get(s) ?? 0) + 1);
1182
+ }
1183
+ lintChildren(node).forEach(tally);
1184
+ };
1185
+ nodes.forEach(tally);
1186
+ const sites = new Map();
1187
+ const visit = (node) => {
1188
+ if ('map' === node.t || 'list' === node.t) {
1189
+ const s = shape(node);
1190
+ if (2 <= counts.get(s) && REPEAT_MIN_WIDTH <= width(s)) {
1191
+ sites.set(s, (sites.get(s) ?? []).concat([node]));
1192
+ return;
1193
+ }
1194
+ }
1195
+ lintChildren(node).forEach(visit);
1196
+ };
1197
+ nodes.forEach(visit);
1198
+ for (const found of sites.values()) {
1199
+ if (2 <= found.length) {
1200
+ const [first, ...rest] = found.map((n) => lineCol(text, n.at));
1201
+ out.push({
1202
+ rule: 'style/repeat', ...first,
1203
+ message: `this ${found[0].t} is written ${found.length} times (again at ` +
1204
+ rest.map((p) => p.line + ':' + p.col).join(', ') +
1205
+ '); an alias would name it once',
1206
+ });
1207
+ }
1208
+ }
1209
+ }
1210
+ // A node's shape: its spelling with the layout, the comments and, for
1211
+ // a map, the order of its entries taken out, so that two spellings of
1212
+ // one value are one shape, as they are one canon.
1213
+ function shape(node) {
1214
+ switch (node.t) {
1215
+ case 'map':
1216
+ return '{' + node.body.filter(shaped).map((e) => shape(lintNode(e))).sort().join(' ') + '}';
1217
+ case 'list':
1218
+ return '[' + node.body.filter(shaped).map((e) => shape(lintNode(e))).join(' ') + ']';
1219
+ case 'pair':
1220
+ return node.key + (node.opt ? '?' : '') + ':' + shape(node.value);
1221
+ case 'spread':
1222
+ return '&:' + shape(node.value);
1223
+ case 'call':
1224
+ return node.name + '(' + node.args.filter(shaped).map(shape).join(',') + ')';
1225
+ case 'paren':
1226
+ return '(' + node.inner.filter(shaped).map(shape).join(',') + ')';
1227
+ case 'expr':
1228
+ return node.items.filter(shaped).map(shape).join('');
1229
+ default:
1230
+ return node.text;
1231
+ }
1232
+ }
1233
+ function shaped(node) {
1234
+ return 'comment' !== node.t && 'blank' !== node.t && 'note' !== node.t;
1235
+ }
1236
+ // ---------------------------------------------------------------------
774
1237
  // The verb's library surface
775
1238
  function lf(text) {
776
1239
  return text.split('\r\n').join('\n');
777
1240
  }
778
1241
  // The check: the output parses, and to the same tree. Pre-unification
779
- // canon is that tree, positions aside, and every rewrite of this tier
780
- // leaves it unchanged (§7.3).
1242
+ // canon is that tree, positions aside, and every rewrite of the
1243
+ // syntactic tier leaves it unchanged (§7.3).
781
1244
  function sameDocument(root, after) {
782
1245
  const p = parseDoc(after, undefined, undefined);
783
1246
  return undefined === p.errors && root.canon === p.root.canon;
784
1247
  }
1248
+ // The check of a lawful rewrite: the spelling before and the spelling
1249
+ // after, evaluated in isolation, come to the same canon, the same
1250
+ // kinds of failure, and the same outcome of generation (§7.3). Local,
1251
+ // so it needs no include and no capability, and it applies whether or
1252
+ // not the document as a whole evaluates. The kinds, not the count: how
1253
+ // often one unresolved reference is reported depends on the order the
1254
+ // meet took. Generation too, because the engine generates from more
1255
+ // than the canon: a meet of maps with a nil member has refused a key
1256
+ // the same map written once generates.
1257
+ function sameByMeet(before, after) {
1258
+ return meetOf(before) === meetOf(after);
1259
+ }
1260
+ function meetOf(text) {
1261
+ const aontu = engine();
1262
+ const ctx = aontu.ctx({ collect: true });
1263
+ const v = aontu.unify(text, undefined, ctx);
1264
+ const gen = aontu.ctx({ collect: true });
1265
+ const out = aontu.generate(text, undefined, gen);
1266
+ const outcome = undefined !== out ? 'generated'
1267
+ : 0 < ctx.err.length ? kinds(ctx.err) : gen.err[0].why;
1268
+ return v.canon + '\n' + kinds(ctx.err) + '\n' + outcome;
1269
+ }
1270
+ function kinds(errs) {
1271
+ const whys = errs.map((e) => e.why);
1272
+ return whys.filter((x, i) => i === whys.indexOf(x)).sort().join(',');
1273
+ }
785
1274
  function depthFinding() {
786
1275
  return {
787
1276
  code: 'max_depth',
@@ -817,19 +1306,25 @@ function format(src, opts, hooks) {
817
1306
  return { verdict: 'error', errors: parsed.errors };
818
1307
  }
819
1308
  const reader = new Reader(toks);
820
- const root = reader.body('', false).body;
1309
+ const root = unwrap(reader.body('', false).body);
821
1310
  if (reader.deep) {
822
1311
  return { verdict: 'error', errors: [depthFinding()] };
823
1312
  }
824
- const out = emit(unwrap(root));
1313
+ // The syntactic tier first, checked against the parse tree; then the
1314
+ // lawful tier over it, each rewrite checked by the meet.
1315
+ const plain = emit(root, undefined);
825
1316
  const same = hooks?.same ?? sameDocument;
826
- if (!same(parsed.root, out)) {
1317
+ if (!same(parsed.root, plain)) {
827
1318
  return {
828
1319
  verdict: 'error',
829
- errors: [checkFinding(opts?.path, parsed.root.canon, out)],
1320
+ errors: [checkFinding(opts?.path, parsed.root.canon, plain)],
830
1321
  };
831
1322
  }
832
- return { verdict: 'formatted', text: out, changed: out !== src };
1323
+ const out = emit(root, hooks?.meet ?? sameByMeet);
1324
+ return {
1325
+ verdict: 'formatted', text: out, changed: out !== src,
1326
+ findings: opts?.lint ? lintOf(root, text) : [],
1327
+ };
833
1328
  }
834
1329
  // The lines of a text, with a marker on the last when the text does
835
1330
  // not end in a newline: such a line never equals its