aontu 0.59.0 → 0.61.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.
- package/dist/aontu.d.ts +1 -1
- package/dist/aontu.js +1 -1
- package/dist/cli.js +59 -23
- package/dist/cli.js.map +1 -1
- package/dist/format.d.ts +1 -0
- package/dist/format.js +97 -11
- package/dist/format.js.map +1 -1
- package/dist/hcanon.js +32 -4
- package/dist/hcanon.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/std.js +8 -0
- package/dist/std.js.map +1 -1
- package/dist/template.d.ts +2 -1
- package/dist/template.js +51 -15
- package/dist/template.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/val/RecurseVal.js +11 -0
- package/dist/val/RecurseVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -0
- package/dist/val/RefVal.js +14 -2
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.js +24 -10
- package/dist/val/ReferFuncVal.js.map +1 -1
- package/package.json +1 -1
- package/src/aontu.ts +1 -1
- package/src/cli.ts +65 -25
- package/src/format.ts +116 -11
- package/src/hcanon.ts +32 -4
- package/src/std.ts +8 -0
- package/src/template.ts +55 -15
- package/src/val/RecurseVal.ts +11 -0
- package/src/val/RefVal.ts +16 -2
- package/src/val/ReferFuncVal.ts +24 -10
package/src/format.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { Aontu } from './aontu'
|
|
|
26
26
|
import { failureFinding } from './vet'
|
|
27
27
|
import type { VetFinding } from './vet'
|
|
28
28
|
import type { Resolver } from './type'
|
|
29
|
+
import { desugarTemplate, resugarTemplate, templateOutputs } from './template'
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
// The packing budget (§3.1). It decides which of two legal spellings
|
|
@@ -46,6 +47,12 @@ export type FormatOptions = {
|
|
|
46
47
|
// Report the style findings of §4 -- key case, repeated shapes --
|
|
47
48
|
// beside the text. The formatter never acts on them.
|
|
48
49
|
lint?: boolean
|
|
50
|
+
// THE SOURCE IS A GENERATOR, and this is its marker
|
|
51
|
+
// (docs/design/TEMPLATE.0.md; FMT.0.md §3.14). The file is desugared,
|
|
52
|
+
// formatted and resugared, so `text` is a template again -- the aontu
|
|
53
|
+
// in the agreed form, indented after the marker, and every line of
|
|
54
|
+
// output exactly where it was.
|
|
55
|
+
template?: string
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
// A style finding (§4): what the formatter points at and never
|
|
@@ -183,6 +190,10 @@ type Node = {
|
|
|
183
190
|
|
|
184
191
|
// The source index of the node's first token: the lint's positions.
|
|
185
192
|
at?: number
|
|
193
|
+
|
|
194
|
+
// The node begins a line of the target's own file (§3.14), so it has
|
|
195
|
+
// no one-line form and every container holding it opens.
|
|
196
|
+
held?: boolean
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
const BINARY: Record<string, boolean> = { '#E&': true, '#E|': true, '#E+': true }
|
|
@@ -608,7 +619,7 @@ function pairHead(node: Node, tight: boolean): string {
|
|
|
608
619
|
// lines. `tight` is the inline form of a pair, `a:1`, used inside a
|
|
609
620
|
// container; a statement's pair is `a: 1`.
|
|
610
621
|
function inline(node: Node, tight: boolean): string | undefined {
|
|
611
|
-
if (undefined !== node.trail) {
|
|
622
|
+
if (undefined !== node.trail || node.held) {
|
|
612
623
|
return undefined
|
|
613
624
|
}
|
|
614
625
|
switch (node.t) {
|
|
@@ -744,6 +755,15 @@ class Writer {
|
|
|
744
755
|
return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n'
|
|
745
756
|
}
|
|
746
757
|
|
|
758
|
+
// A blank line above the line at an index: the gap of §3.8, opened
|
|
759
|
+
// once the statement below it turns out to be a tree. Never at the
|
|
760
|
+
// top of the page, and never a second time.
|
|
761
|
+
gap(at: number): void {
|
|
762
|
+
if (0 < at && '' !== this.lines[at - 1]) {
|
|
763
|
+
this.lines.splice(at, 0, '')
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
747
767
|
// The lines since a mark replaced by a text: the spelling before,
|
|
748
768
|
// where a rewrite did not pass its check.
|
|
749
769
|
replace(mark: number, text: string): void {
|
|
@@ -776,29 +796,57 @@ function rtrim(s: string): string {
|
|
|
776
796
|
// body of a plain map that is itself the value of a statement) a pair
|
|
777
797
|
// is laid out by §3.4, which may repeat its key; anywhere else -- a
|
|
778
798
|
// list, an operand, an argument -- by §3.5 alone.
|
|
779
|
-
function emitBody(
|
|
799
|
+
function emitBody(
|
|
800
|
+
w: Writer, body: Node[], indent: number, stmt: Stmt | undefined, root?: boolean
|
|
801
|
+
): void {
|
|
780
802
|
let pending = false
|
|
781
803
|
let count = 0
|
|
804
|
+
// Where the run being written begins: a statement, with the comments
|
|
805
|
+
// standing directly above it, so that the gap below opens ABOVE the
|
|
806
|
+
// comments rather than between them and what they describe. A blank
|
|
807
|
+
// line ends a run -- comments across a gap belong to what is above.
|
|
808
|
+
let head = 0
|
|
809
|
+
let noted = false
|
|
782
810
|
for (const node of body) {
|
|
783
811
|
if ('blank' === node.t) {
|
|
784
812
|
pending = 0 < count
|
|
785
813
|
continue
|
|
786
814
|
}
|
|
815
|
+
const gapped = pending
|
|
787
816
|
w.open(indent, pending)
|
|
817
|
+
if (gapped || !noted) {
|
|
818
|
+
head = w.mark()
|
|
819
|
+
}
|
|
788
820
|
pending = false
|
|
789
821
|
count++
|
|
790
822
|
if ('comment' === node.t) {
|
|
823
|
+
noted = true
|
|
791
824
|
w.text(node.text!)
|
|
792
825
|
continue
|
|
793
826
|
}
|
|
827
|
+
noted = false
|
|
828
|
+
const from = w.mark()
|
|
794
829
|
if (undefined !== stmt && 'pair' === node.t) {
|
|
795
830
|
emitStatement(w, node, indent, stmt, '')
|
|
796
|
-
continue
|
|
797
831
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
832
|
+
else {
|
|
833
|
+
const e = chain(node)
|
|
834
|
+
emitValue(w, e, indent)
|
|
835
|
+
if (undefined !== e.trail) {
|
|
836
|
+
w.text(' ' + e.trail)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// A TOP-LEVEL STATEMENT WRITTEN AS A TREE STANDS APART (§3.8). A
|
|
840
|
+
// document states several things -- a service, then its entities,
|
|
841
|
+
// then its errors -- and where one of them is a tree rather than a
|
|
842
|
+
// line, the eye finds it by the space around it. What counts as a
|
|
843
|
+
// tree is measured rather than guessed: the statement took more
|
|
844
|
+
// than one line to write. So `a: 1` beside `b: 2` is left alone,
|
|
845
|
+
// and this rule cannot fire below the root, where a blank line is
|
|
846
|
+
// the author's (§3.8) and nothing else.
|
|
847
|
+
if (root && from < w.mark()) {
|
|
848
|
+
w.gap(head)
|
|
849
|
+
pending = true
|
|
802
850
|
}
|
|
803
851
|
}
|
|
804
852
|
}
|
|
@@ -1282,7 +1330,7 @@ function emitAt(nodes: Node[], indent: number): string {
|
|
|
1282
1330
|
function emit(root: Node[], meet: Meet | undefined): string {
|
|
1283
1331
|
const w = new Writer()
|
|
1284
1332
|
emitBody(w, undefined === meet ? root : mergeRuns(root), 0,
|
|
1285
|
-
undefined === meet ? undefined : { meet, covered: false })
|
|
1333
|
+
undefined === meet ? undefined : { meet, covered: false }, true)
|
|
1286
1334
|
return w.finish()
|
|
1287
1335
|
}
|
|
1288
1336
|
|
|
@@ -1516,13 +1564,66 @@ function checkFinding(path: string | undefined, expected: string, actual: string
|
|
|
1516
1564
|
}
|
|
1517
1565
|
}
|
|
1518
1566
|
|
|
1567
|
+
// THE TARGET'S OWN LINES ARE HELD ON LINES OF THEIR OWN (§3.14). The
|
|
1568
|
+
// desugaring is line for line, so a line of output is known by the
|
|
1569
|
+
// offset it begins at, and the node beginning there -- the quoted
|
|
1570
|
+
// string the desugaring wrote -- is marked. From there on it has no
|
|
1571
|
+
// one-line form, so every container holding it opens, and no two lines
|
|
1572
|
+
// of the generated file are ever packed onto one.
|
|
1573
|
+
function holdOutput(nodes: Node[], at: Set<number>): void {
|
|
1574
|
+
for (const node of nodes) {
|
|
1575
|
+
if (undefined !== node.at && at.has(node.at)) {
|
|
1576
|
+
node.held = true
|
|
1577
|
+
}
|
|
1578
|
+
holdOutput(node.body ?? [], at)
|
|
1579
|
+
holdOutput(node.args ?? [], at)
|
|
1580
|
+
holdOutput(node.inner ?? [], at)
|
|
1581
|
+
holdOutput(node.items ?? [], at)
|
|
1582
|
+
if (undefined !== node.value) {
|
|
1583
|
+
holdOutput([node.value], at)
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
// The offsets the flagged lines of a document begin at.
|
|
1590
|
+
function outputAt(doc: string, flags: boolean[]): Set<number> {
|
|
1591
|
+
const at = new Set<number>()
|
|
1592
|
+
let off = 0
|
|
1593
|
+
const lines = doc.split('\n')
|
|
1594
|
+
for (let k = 0; k < lines.length; k++) {
|
|
1595
|
+
if (flags[k]) {
|
|
1596
|
+
at.add(off)
|
|
1597
|
+
}
|
|
1598
|
+
off += lines[k].length + 1
|
|
1599
|
+
}
|
|
1600
|
+
return at
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
// A finding's column in the TEMPLATE rather than in the document it
|
|
1605
|
+
// carries (§3.14): the marker and its one space stand before the aontu
|
|
1606
|
+
// on every line the resugaring writes. Every finding is on such a
|
|
1607
|
+
// line -- the two rules point at a key or at a container, and a line of
|
|
1608
|
+
// output is a bare string, which is neither.
|
|
1609
|
+
function shiftFindings(findings: LintFinding[], mark: string | undefined): LintFinding[] {
|
|
1610
|
+
return undefined === mark ? findings :
|
|
1611
|
+
findings.map((f) => ({ ...f, col: f.col + mark.length + 1 }))
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
|
|
1519
1615
|
// Format one document. The text is the agreed form of the source;
|
|
1520
1616
|
// `changed` says whether it differs from what was given, which is
|
|
1521
1617
|
// what `--check` and `--list` report.
|
|
1522
1618
|
export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks): FormatReport {
|
|
1523
1619
|
const text = lf(src)
|
|
1620
|
+
// A GENERATOR IS FORMATTED AS THE DOCUMENT IT CARRIES (§3.14): the
|
|
1621
|
+
// template surface's two transforms stand either side of the
|
|
1622
|
+
// formatter, and between them is what happens to any other document.
|
|
1623
|
+
const mark = opts?.template
|
|
1624
|
+
const doc = undefined === mark ? text : desugarTemplate(text, mark)
|
|
1524
1625
|
const toks: Tok[] = []
|
|
1525
|
-
const parsed = parseDoc(
|
|
1626
|
+
const parsed = parseDoc(doc, opts?.path, toks)
|
|
1526
1627
|
if (undefined !== parsed.errors) {
|
|
1527
1628
|
return { verdict: 'error', errors: parsed.errors }
|
|
1528
1629
|
}
|
|
@@ -1531,6 +1632,9 @@ export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks):
|
|
|
1531
1632
|
if (reader.deep) {
|
|
1532
1633
|
return { verdict: 'error', errors: [depthFinding()] }
|
|
1533
1634
|
}
|
|
1635
|
+
if (undefined !== mark) {
|
|
1636
|
+
holdOutput(root, outputAt(doc, templateOutputs(text, mark)))
|
|
1637
|
+
}
|
|
1534
1638
|
// The syntactic tier first, checked against the parse tree; then the
|
|
1535
1639
|
// lawful tier over it, each rewrite checked by the meet.
|
|
1536
1640
|
const plain = emit(root, undefined)
|
|
@@ -1542,9 +1646,10 @@ export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks):
|
|
|
1542
1646
|
}
|
|
1543
1647
|
}
|
|
1544
1648
|
const out = emit(root, hooks?.meet ?? sameByMeet)
|
|
1649
|
+
const done = undefined === mark ? out : resugarTemplate(out, mark)
|
|
1545
1650
|
return {
|
|
1546
|
-
verdict: 'formatted', text:
|
|
1547
|
-
findings: opts?.lint ? lintOf(root,
|
|
1651
|
+
verdict: 'formatted', text: done, changed: done !== src,
|
|
1652
|
+
findings: opts?.lint ? shiftFindings(lintOf(root, doc), mark) : [],
|
|
1548
1653
|
}
|
|
1549
1654
|
}
|
|
1550
1655
|
|
package/src/hcanon.ts
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
// (test/spec/hcanon.tsv). User-facing canon is UNCHANGED — hcanon is a
|
|
17
17
|
// separate rendering.
|
|
18
18
|
//
|
|
19
|
+
// An ALIAS REFERENCE left standing in a spread template renders its
|
|
20
|
+
// EXPANSION through this same walk rather than through the reference's
|
|
21
|
+
// own canon, so a close() or a mark on the aliased value survives into
|
|
22
|
+
// the hash. Without that the wrappers above are absent exactly where
|
|
23
|
+
// the alias filter has already erased the declaration that carried
|
|
24
|
+
// them (BUGS.md §60).
|
|
25
|
+
//
|
|
19
26
|
// The marks PROPAGATE to every descendant at unification (walkMark), so
|
|
20
27
|
// a wrapper is emitted only where a mark STARTS: the walk carries the
|
|
21
28
|
// inherited marks down and a child whose mark the parent already
|
|
@@ -49,10 +56,12 @@ type HMarks = {
|
|
|
49
56
|
// One node's rendering, carrying the marks the ANCESTORS already
|
|
50
57
|
// wrapped. Bags and junctions recurse structurally (their canon getters
|
|
51
58
|
// render children through plain canon, which would drop a nested
|
|
52
|
-
// close)
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// is
|
|
59
|
+
// close), and so does an expanded alias reference, for the reason its
|
|
60
|
+
// own arm gives. Everything else — scalars, kinds, funcs, constraints,
|
|
61
|
+
// and a reference with nothing to expand — delegates to its own canon,
|
|
62
|
+
// whose text is already in cross-port parity. The non-Val arm mirrors
|
|
63
|
+
// MapVal.canon's raw-peg fallback and is unreachable through an
|
|
64
|
+
// evaluated tree (direct-tested, ADR-002).
|
|
56
65
|
function render(v: any, inh: HMarks): string {
|
|
57
66
|
if (true !== v?.isVal) {
|
|
58
67
|
return String(v)
|
|
@@ -106,6 +115,25 @@ function render(v: any, inh: HMarks): string {
|
|
|
106
115
|
else if (true === v.isConjunct || true === v.isDisjunct) {
|
|
107
116
|
s = junctionText(v, true === v.isConjunct ? '&' : '|', inner)
|
|
108
117
|
}
|
|
118
|
+
// AN EXPANDED ALIAS REFERENCE IS RENDERED, NOT DELEGATED (BUGS.md
|
|
119
|
+
// §60). A reference standing after unification is one inside a
|
|
120
|
+
// spread template, and `RefVal.canon` answers with the EXPANSION's
|
|
121
|
+
// plain canon -- which drops exactly what this renderer exists to
|
|
122
|
+
// keep. The declaration carrying `close()` or a mark is erased by
|
|
123
|
+
// the alias filter above, so a `close()` lost here is lost from the
|
|
124
|
+
// hash entirely: `%A = close({n:string})` and `%A = {n:string}`,
|
|
125
|
+
// used as `box: [&: %A]`, hashed to ONE STRING while refusing and
|
|
126
|
+
// admitting `{n:"x",z:1}` respectively -- a change of meaning the
|
|
127
|
+
// pin reported as no change, in the unsafe direction. Recursing
|
|
128
|
+
// through `inner` is what makes the alias form and its longhand
|
|
129
|
+
// twin agree again, which is ALIASES.0.md §4's own requirement.
|
|
130
|
+
//
|
|
131
|
+
// A reference with NO expansion still spells its name: a plain
|
|
132
|
+
// `$.A` names a key the hash form still carries in full, and the
|
|
133
|
+
// knot of a recursive alias inside its own template never gets one.
|
|
134
|
+
else if (true === v.isRef && undefined !== v.expansion) {
|
|
135
|
+
s = render(v.expansion, inner)
|
|
136
|
+
}
|
|
109
137
|
else {
|
|
110
138
|
s = v.canon
|
|
111
139
|
}
|
package/src/std.ts
CHANGED
|
@@ -186,6 +186,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
186
186
|
k: "prim"
|
|
187
187
|
prim: "string" | "int" | "bigint" | "float" | "decimal" | "bool" | "null" | "any"
|
|
188
188
|
})
|
|
189
|
+
|
|
189
190
|
%ref = close({ k:"ref" name:%name unit?:string })
|
|
190
191
|
%leaf = %prim | %ref | %text
|
|
191
192
|
|
|
@@ -208,6 +209,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
208
209
|
})
|
|
209
210
|
|
|
210
211
|
%member = close({ name:%name value?:string | number doc?:%doc x?:{} })
|
|
212
|
+
|
|
211
213
|
%param = close({
|
|
212
214
|
name: %name
|
|
213
215
|
type: %type
|
|
@@ -221,12 +223,14 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
221
223
|
%inline = string & re("^[^\n\r]*$") | %ref
|
|
222
224
|
%line = close({ k:"line" at:*0 | integer & min(0) & max(64) of:[&: %inline] })
|
|
223
225
|
%blank = close({ k:"blank" n:*1 | integer & min(1) & max(16) })
|
|
226
|
+
|
|
224
227
|
%raw = close({
|
|
225
228
|
k: "raw"
|
|
226
229
|
at: *0 | integer & min(0) & max(64)
|
|
227
230
|
text: string
|
|
228
231
|
reindent: *true | boolean
|
|
229
232
|
})
|
|
233
|
+
|
|
230
234
|
%piece = %line | %blank | %raw | string & re("^[^\n\r]*$")
|
|
231
235
|
%frag = close({ k:"frag" of:[&: %piece] })
|
|
232
236
|
%body = %frag | close({ k:"abstract" })
|
|
@@ -241,7 +245,9 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
241
245
|
check?: [&: %check]
|
|
242
246
|
x?: {}
|
|
243
247
|
})
|
|
248
|
+
|
|
244
249
|
%enum = close({ k:"enum" name:%name doc?:%doc members:[&: %member] x?:{} })
|
|
250
|
+
|
|
245
251
|
%alias = close({
|
|
246
252
|
k: "alias"
|
|
247
253
|
name: %name
|
|
@@ -250,6 +256,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
250
256
|
check?: [&: %check]
|
|
251
257
|
x?: {}
|
|
252
258
|
})
|
|
259
|
+
|
|
253
260
|
%const = close({
|
|
254
261
|
k: "const"
|
|
255
262
|
name: %name
|
|
@@ -258,6 +265,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
|
|
|
258
265
|
value: string | number | boolean | null
|
|
259
266
|
x?: {}
|
|
260
267
|
})
|
|
268
|
+
|
|
261
269
|
%func = close({
|
|
262
270
|
k: "func"
|
|
263
271
|
name: %name
|
package/src/template.ts
CHANGED
|
@@ -24,12 +24,22 @@
|
|
|
24
24
|
// comment, and derives for a language this table has never seen: the
|
|
25
25
|
// caller passes the token.
|
|
26
26
|
//
|
|
27
|
-
// A MARKER IS RECOGNISED AFTER LEADING WHITESPACE AND
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// indented two spaces in the
|
|
31
|
-
//
|
|
32
|
-
// the
|
|
27
|
+
// A MARKER IS RECOGNISED AFTER LEADING WHITESPACE, AND THE INDENTATION
|
|
28
|
+
// THE RESUGARING WRITES IS THE AONTU'S (D2, amended 2026-09-07). An
|
|
29
|
+
// output line is verbatim -- a line indented two spaces in the template
|
|
30
|
+
// is indented two spaces in the generated file (D7) -- and that is the
|
|
31
|
+
// half a reader of the TARGET needs. The other half is the document:
|
|
32
|
+
// the marker lines carry a tree, and a tree nobody can see the shape of
|
|
33
|
+
// is the one thing this surface took away. So the marker stands at the
|
|
34
|
+
// left margin and the aontu is indented AFTER it, which `aontu fmt`
|
|
35
|
+
// (FMT.0.md M-BM-'3.14) is what writes.
|
|
36
|
+
//
|
|
37
|
+
// Reading is unchanged and stays generous: a marker after leading
|
|
38
|
+
// whitespace is a marker, and its own indentation still counts toward
|
|
39
|
+
// the aontu it carries, so a template written the other way desugars to
|
|
40
|
+
// the same document. What changes is the spelling the round trip
|
|
41
|
+
// answers, and `template --check` names the difference as the drift it
|
|
42
|
+
// is.
|
|
33
43
|
//
|
|
34
44
|
// THE SUGAR IS THE FIXPOINT OF THE TWO TRANSFORMS (D6), and that is
|
|
35
45
|
// the whole of the resugaring's safety. A canonical line that looks
|
|
@@ -128,13 +138,19 @@ function indentOf(line: string): number {
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
|
|
131
|
-
// The line without its
|
|
132
|
-
function
|
|
141
|
+
// The line without its trailing spaces and tabs.
|
|
142
|
+
function trimEnd(line: string): string {
|
|
133
143
|
let end = line.length
|
|
134
144
|
while (0 < end && (' ' === line[end - 1] || '\t' === line[end - 1])) {
|
|
135
145
|
end--
|
|
136
146
|
}
|
|
137
|
-
return line.slice(
|
|
147
|
+
return line.slice(0, end)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// The line without its indentation or its trailing spaces and tabs.
|
|
152
|
+
function trimLine(line: string): string {
|
|
153
|
+
return trimEnd(line).slice(indentOf(line))
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
|
|
@@ -159,12 +175,15 @@ function readLine(line: string, marker: string): Line {
|
|
|
159
175
|
// The block form's closer is part of the marker, not of the aontu.
|
|
160
176
|
// A block marker line that never closes is not a marker line: the
|
|
161
177
|
// language's own parser would not read it as a comment either.
|
|
178
|
+
// Only the TRAILING space goes: what stands between the opener and
|
|
179
|
+
// the aontu is the aontu's indentation, exactly as it is for a line
|
|
180
|
+
// marker, and the one-space rule below takes the marker's own.
|
|
162
181
|
if (isBlock(marker)) {
|
|
163
182
|
const end = body.lastIndexOf(BLOCK_CLOSE)
|
|
164
183
|
if (end < 0) {
|
|
165
184
|
return { marker: false, indent: '', text: line }
|
|
166
185
|
}
|
|
167
|
-
body =
|
|
186
|
+
body = trimEnd(body.slice(0, end))
|
|
168
187
|
}
|
|
169
188
|
// ONE SPACE AFTER THE MARKER IS THE MARKER'S, so `//- x: 1` carries
|
|
170
189
|
// `x: 1` and the resugaring writes the space back. A marker written
|
|
@@ -273,19 +292,40 @@ function resugarTemplate(src: string, marker?: string): string {
|
|
|
273
292
|
if (undefined !== target && desugarTemplate(target, mark) === trimLine(line)) {
|
|
274
293
|
return target
|
|
275
294
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
295
|
+
// THE MARKER STANDS AT THE LEFT MARGIN and the line's indentation
|
|
296
|
+
// is written after it, so the aontu's own shape is on the page.
|
|
297
|
+
// The one space is the marker's, which the reading takes back.
|
|
298
|
+
const text = trimEnd(line)
|
|
279
299
|
const close = isBlock(mark) ? ' ' + BLOCK_CLOSE : ''
|
|
280
|
-
return '' === text ?
|
|
300
|
+
return '' === text ? mark + close : mark + ' ' + text + close
|
|
281
301
|
})
|
|
282
302
|
return out.join('\n') + (tail ? '\n' : '')
|
|
283
|
-
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
// WHICH LINES OF THE DESUGARED DOCUMENT ARE THE TARGET'S. The
|
|
307
|
+
// desugaring is line for line, so this is one flag per line of what
|
|
308
|
+
// `desugarTemplate` returns: true where the template's line was not a
|
|
309
|
+
// marker, and so where the document's line is one quoted line of the
|
|
310
|
+
// generated file.
|
|
311
|
+
//
|
|
312
|
+
// `aontu fmt` reads it (FMT.0.md M-BM-'3.14) to hold those lines on lines of
|
|
313
|
+
// their own. A body element is a string like any other, and the packing
|
|
314
|
+
// budget would put three of them on one line -- which is aontu where
|
|
315
|
+
// three lines of output were, and a generator that writes them as one.
|
|
316
|
+
function templateOutputs(src: string, marker: string): boolean[] {
|
|
317
|
+
const lines = src.split('\n')
|
|
318
|
+
if (1 < lines.length && '' === lines[lines.length - 1]) {
|
|
319
|
+
lines.pop()
|
|
320
|
+
}
|
|
321
|
+
return lines.map((line) => !readLine(line, marker).marker)
|
|
322
|
+
} /* node:coverage ignore next 9 */
|
|
284
323
|
|
|
285
324
|
|
|
286
325
|
export {
|
|
287
326
|
desugarTemplate,
|
|
288
327
|
resugarTemplate,
|
|
328
|
+
templateOutputs,
|
|
289
329
|
markerFor,
|
|
290
330
|
DEFAULT_MARKER,
|
|
291
331
|
}
|
package/src/val/RecurseVal.ts
CHANGED
|
@@ -204,6 +204,17 @@ function bumpRecurse(v: any, xc: number): void {
|
|
|
204
204
|
v.xc = Math.max(v.xc, xc)
|
|
205
205
|
return
|
|
206
206
|
}
|
|
207
|
+
// A RAW REFERENCE TO A RECURSIVE TARGET IS THE RECURSION, minted or
|
|
208
|
+
// not -- the same reading containsRecurseOf already takes, and the
|
|
209
|
+
// reason bound 2 was inert. A freshly cloned level holds the
|
|
210
|
+
// definition's references UNRESOLVED, so this walk found no
|
|
211
|
+
// residual to stamp and `xc` read 0 at every expansion, in the
|
|
212
|
+
// healthy form too. The seed rides on the reference and the
|
|
213
|
+
// residual minted from it starts there (ts/src/val/RefVal.ts).
|
|
214
|
+
if (true === v.isRef) {
|
|
215
|
+
v.rxc = Math.max(v.rxc ?? 0, xc)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
207
218
|
const peg: any = v.peg
|
|
208
219
|
if (true === v.isMap && null != peg) {
|
|
209
220
|
for (const k of Object.keys(peg)) {
|
package/src/val/RefVal.ts
CHANGED
|
@@ -107,6 +107,15 @@ class RefVal extends FeatureVal {
|
|
|
107
107
|
// after unification (see `canon` below). Not a ValSpec field: it is
|
|
108
108
|
// a rendering of the settled tree, never a parse-time property.
|
|
109
109
|
expansion: Val | undefined = undefined
|
|
110
|
+
|
|
111
|
+
// THE RECURSION SEED (use-cases/BUGS.md §57). A reference that names
|
|
112
|
+
// a recursive definition IS the fixpoint reference; it mints a
|
|
113
|
+
// RecurseVal when it resolves. bumpRecurse stamps the expansion
|
|
114
|
+
// depth here, because a freshly cloned level holds the definition's
|
|
115
|
+
// references UNRESOLVED and so has no residual to stamp -- which is
|
|
116
|
+
// why the design's second termination bound read 0 at every
|
|
117
|
+
// expansion. The minted residual starts from this.
|
|
118
|
+
rxc: number = 0
|
|
110
119
|
prefix: boolean = false
|
|
111
120
|
|
|
112
121
|
constructor(
|
|
@@ -329,7 +338,7 @@ class RefVal extends FeatureVal {
|
|
|
329
338
|
out = makeNilErr(ctx, 'path_cycle', this)
|
|
330
339
|
}
|
|
331
340
|
else {
|
|
332
|
-
const rec: any = new RecurseVal({ target } as any, ctx)
|
|
341
|
+
const rec: any = new RecurseVal({ target, xc: this.rxc } as any, ctx)
|
|
333
342
|
rec.site = this.site
|
|
334
343
|
rec.path = [...this.path]
|
|
335
344
|
out = rec
|
|
@@ -610,7 +619,8 @@ class RefVal extends FeatureVal {
|
|
|
610
619
|
// residual is the resolved form, exactly as at the prefix
|
|
611
620
|
// positions inside the definition.
|
|
612
621
|
else if (null != out && !snap && containsRecurseOf(out, this.peg as any)) {
|
|
613
|
-
const rec: any = new RecurseVal(
|
|
622
|
+
const rec: any = new RecurseVal(
|
|
623
|
+
{ target: [...this.peg], xc: this.rxc } as any, ctx)
|
|
614
624
|
rec.site = this.site
|
|
615
625
|
rec.path = [...this.path]
|
|
616
626
|
out = rec
|
|
@@ -829,6 +839,10 @@ class RefVal extends FeatureVal {
|
|
|
829
839
|
...(spec || {})
|
|
830
840
|
}) as RefVal)
|
|
831
841
|
out.expansion = this.expansion
|
|
842
|
+
// The recursion seed travels with the clone: a spread template is
|
|
843
|
+
// cloned per destination, and each clone's residual must start
|
|
844
|
+
// where the level it came from left off.
|
|
845
|
+
out.rxc = this.rxc
|
|
832
846
|
return out
|
|
833
847
|
}
|
|
834
848
|
|
package/src/val/ReferFuncVal.ts
CHANGED
|
@@ -290,13 +290,25 @@ class ReferVal extends FeatureVal {
|
|
|
290
290
|
// resolved string should take.
|
|
291
291
|
settle(ctx: AontuContext, site: Val): Val {
|
|
292
292
|
if (undefined === this.addr) {
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
293
|
+
// DONE while unmet, as `string` and `min(1)` are, and as RelVal
|
|
294
|
+
// has been since it was written (see its constructor). An
|
|
295
|
+
// ADDRESS-LESS refer has nothing to check yet and nothing to
|
|
296
|
+
// refuse: it is its own settled residual, and the meet
|
|
297
|
+
// re-activates it the moment a value arrives, because map
|
|
298
|
+
// merges build the conjunct regardless.
|
|
299
|
+
//
|
|
300
|
+
// NOT-DONE here used to be justified as keeping the pass loop
|
|
301
|
+
// offering the refer a chance -- but that is the job of the
|
|
302
|
+
// OTHER pending branch below, where an address exists and its
|
|
303
|
+
// target has not appeared. This branch has no address to
|
|
304
|
+
// resolve, so staying not-done bought nothing and cost the
|
|
305
|
+
// schema idiom: a `type()` body holding a link never settled,
|
|
306
|
+
// so its mark never transferred, so generation could not skip
|
|
307
|
+
// the marked subtree and raised `mapval_no_gen` naming the
|
|
308
|
+
// definition. `type({p: integer})`, `type({p: path()})` and
|
|
309
|
+
// `type({p: min(1)})` all settled; only a link did not
|
|
310
|
+
// (aontu-lang/aontu#172, G4 phase 4's recorded cost).
|
|
311
|
+
this.dc = DONE
|
|
300
312
|
return this
|
|
301
313
|
}
|
|
302
314
|
// The address is a TREE PATH, resolved from the link's own
|
|
@@ -477,11 +489,13 @@ class RelVal extends FeatureVal {
|
|
|
477
489
|
super(spec, ctx)
|
|
478
490
|
this.tval = (spec as any).tval ?? top()
|
|
479
491
|
this.held = (spec as any).held
|
|
480
|
-
// DONE while unmet, deliberately
|
|
481
|
-
// G4 phase 4 records the cost of: a type() body holding a rel()
|
|
492
|
+
// DONE while unmet, deliberately: a type() body holding a rel()
|
|
482
493
|
// must SETTLE, or the schema idiom (`dependsOn?: rel($.T)` inside
|
|
483
494
|
// a vocabulary) leaves the type unresolved and every reference to
|
|
484
|
-
// it deferring forever.
|
|
495
|
+
// it deferring forever. This was the property refer() lacked --
|
|
496
|
+
// G4 phase 4 recorded the cost -- until 2026-09-07, when an
|
|
497
|
+
// address-less refer was given it too (#172); the two now settle
|
|
498
|
+
// by the same rule, which is what the shared reasoning below is. An unmet rel is its own settled residual,
|
|
485
499
|
// like `min(1)`; the meet re-activates it whenever a value
|
|
486
500
|
// arrives, because map merges build the conjunct regardless.
|
|
487
501
|
this.dc = DONE
|