aontu 0.68.0 → 0.69.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/alias.d.ts +12 -1
- package/dist/alias.js +176 -6
- package/dist/alias.js.map +1 -1
- package/dist/aliasname.d.ts +19 -0
- package/dist/aliasname.js +65 -0
- package/dist/aliasname.js.map +1 -0
- package/dist/aontu.d.ts +1 -1
- package/dist/aontu.js +21 -8
- package/dist/aontu.js.map +1 -1
- package/dist/aontumodel.js +1 -1
- package/dist/aontumodel.js.map +1 -1
- package/dist/ctx.d.ts +1 -0
- package/dist/ctx.js +1 -0
- package/dist/ctx.js.map +1 -1
- package/dist/err.js +18 -1
- package/dist/err.js.map +1 -1
- package/dist/format.js +14 -0
- package/dist/format.js.map +1 -1
- package/dist/hints.js +36 -7
- package/dist/hints.js.map +1 -1
- package/dist/lang.js +289 -35
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +10 -4
- package/dist/lsp.js +116 -7
- package/dist/lsp.js.map +1 -1
- package/dist/site.d.ts +9 -0
- package/dist/site.js +2 -1
- package/dist/site.js.map +1 -1
- package/dist/subsume.js +6 -1
- package/dist/subsume.js.map +1 -1
- package/dist/val/BagVal.d.ts +1 -0
- package/dist/val/BagVal.js +3 -0
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/EmitFuncVal.js +2 -1
- package/dist/val/EmitFuncVal.js.map +1 -1
- package/dist/val/FilterFuncVal.js +1 -1
- package/dist/val/FilterFuncVal.js.map +1 -1
- package/dist/val/FuncBaseVal.d.ts +2 -1
- package/dist/val/FuncBaseVal.js +25 -1
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/MapVal.js +10 -4
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.js +2 -1
- package/dist/val/MatchFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +3 -0
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/RecurseVal.d.ts +1 -0
- package/dist/val/RecurseVal.js +7 -3
- package/dist/val/RecurseVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -0
- package/dist/val/RefVal.js +29 -12
- package/dist/val/RefVal.js.map +1 -1
- package/package.json +1 -1
- package/src/alias.ts +214 -6
- package/src/aliasname.ts +95 -0
- package/src/aontu.ts +23 -9
- package/src/aontumodel.ts +1 -1
- package/src/ctx.ts +4 -1
- package/src/err.ts +19 -1
- package/src/format.ts +14 -0
- package/src/hints.ts +40 -7
- package/src/lang.ts +334 -30
- package/src/lsp.ts +136 -6
- package/src/site.ts +15 -1
- package/src/subsume.ts +9 -2
- package/src/val/BagVal.ts +4 -0
- package/src/val/EmitFuncVal.ts +3 -2
- package/src/val/FilterFuncVal.ts +2 -2
- package/src/val/FuncBaseVal.ts +30 -1
- package/src/val/MapVal.ts +10 -4
- package/src/val/MatchFuncVal.ts +3 -2
- package/src/val/NilVal.ts +3 -0
- package/src/val/RecurseVal.ts +8 -3
- package/src/val/RefVal.ts +31 -12
package/src/alias.ts
CHANGED
|
@@ -3,8 +3,208 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Val } from './type'
|
|
5
5
|
|
|
6
|
+
import { makeNilErr } from './err'
|
|
7
|
+
|
|
6
8
|
import { cmpCodePoint } from './keyorder'
|
|
7
9
|
import { spreadSnapKey } from './val/MapVal'
|
|
10
|
+
import { ALIAS_NAME, aliasSetItems } from './aliasname'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const ALIAS_DECL_RE = new RegExp('^(' + ALIAS_NAME + ')[ \\t]*=(?!=)')
|
|
14
|
+
|
|
15
|
+
// Whether the head IS a set is aliasSetItems's answer, not the shape's.
|
|
16
|
+
const ALIAS_TAKE_RE = /^(\{[^}]*\})[ \t]*=[ \t]*@[ \t]*"([^"]*)"/
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
type AliasBinding = {
|
|
20
|
+
name: string
|
|
21
|
+
row: number
|
|
22
|
+
col: number
|
|
23
|
+
decl: string
|
|
24
|
+
from: string // the include a destructure took it from, '' if local
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// THE NAMES A FILE BINDS, from its TEXT rather than its tree: an editor
|
|
29
|
+
// asks while the document is half-written and would not parse.
|
|
30
|
+
function aliasScope(src: string): AliasBinding[] {
|
|
31
|
+
const out: AliasBinding[] = []
|
|
32
|
+
const lines = src.split('\n')
|
|
33
|
+
|
|
34
|
+
for (let li = 0; li < lines.length; li++) {
|
|
35
|
+
const line = lines[li]
|
|
36
|
+
const lead = line.length - line.replace(/^[ \t]+/, '').length
|
|
37
|
+
const rest = line.substring(lead)
|
|
38
|
+
const decl = line.trim()
|
|
39
|
+
|
|
40
|
+
const dm = ALIAS_DECL_RE.exec(rest)
|
|
41
|
+
if (null != dm) {
|
|
42
|
+
out.push({ name: dm[1], row: li + 1, col: lead + 1, decl, from: '' })
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tm = ALIAS_TAKE_RE.exec(rest)
|
|
47
|
+
const binds = null == tm ? undefined : aliasSetItems(tm[1])
|
|
48
|
+
if (null == tm || undefined === binds) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The column is each item's own, so a set jumps to the name asked
|
|
53
|
+
// for rather than to the pattern.
|
|
54
|
+
let at = lead
|
|
55
|
+
for (const b of binds) {
|
|
56
|
+
at = line.indexOf(b.local, at)
|
|
57
|
+
out.push({ name: b.local, row: li + 1, col: at + 1, decl, from: tm[2] })
|
|
58
|
+
at += b.local.length
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// EVERY ALIAS REFERENCE NAMES A DECLARED NAME, whether or not anything
|
|
67
|
+
// reaches it. Resolution is lazy, so a reference inside a template that
|
|
68
|
+
// nothing instantiates is never tried and a misspelling compiles clean.
|
|
69
|
+
// Whether a NAME is declared does not depend on what the tree holds, so
|
|
70
|
+
// it is answered here instead. See docs/design/ALIASES.0.md
|
|
71
|
+
function aliasErrors(ctx: any, root: Val): void {
|
|
72
|
+
if (true !== (root as any).isMap) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const declared = new Set<string>((root as any).aliasKeys)
|
|
76
|
+
const seen = new Set<Val>()
|
|
77
|
+
|
|
78
|
+
const visit = (v: any): void => {
|
|
79
|
+
if (null == v || true !== v.isVal || seen.has(v)) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
seen.add(v)
|
|
83
|
+
|
|
84
|
+
if (true === v.isRef) {
|
|
85
|
+
const key: string | undefined = v.aliasKey
|
|
86
|
+
if (undefined !== key && !declared.has(key)) {
|
|
87
|
+
ctx.adderr(makeNilErr(ctx, 'no_path', v, undefined, 'resolve'))
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (true === v.isMap) {
|
|
93
|
+
for (const k of Object.keys(v.peg)) {
|
|
94
|
+
visit(v.peg[k])
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (Array.isArray(v.peg)) {
|
|
98
|
+
for (const e of v.peg) {
|
|
99
|
+
visit(e)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (null != v.peg && true === v.peg.isVal) {
|
|
103
|
+
visit(v.peg)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ((true === v.isMap || true === v.isList) && null != v.spread.cj) {
|
|
107
|
+
visit(v.spread.cj)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
visit(root)
|
|
112
|
+
} /* node:coverage ignore next 3 */
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// T-1 (ALIASES.0.md sections 7 and 9). Expansion TERMINATES -- no
|
|
116
|
+
// parameters, no recursion, a finite name set -- but a name that names
|
|
117
|
+
// names expands to the product of what they hold. The budget is on
|
|
118
|
+
// EXPANDED SIZE and charged here, ahead of evaluation.
|
|
119
|
+
function aliasBudget(ctx: any, root: Val): Val | undefined {
|
|
120
|
+
// A DOCUMENT THAT INCLUDES parses to a conjunct, not a map: the
|
|
121
|
+
// deferred terms are where an included file's names arrive, so the
|
|
122
|
+
// budget must see all of them, not only the first.
|
|
123
|
+
const maps: any[] = []
|
|
124
|
+
const gather = (v: any): void => {
|
|
125
|
+
if (true === v?.isMap) {
|
|
126
|
+
maps.push(v)
|
|
127
|
+
}
|
|
128
|
+
else if (true === v?.isConjunct && Array.isArray(v.peg)) {
|
|
129
|
+
for (const t of v.peg) {
|
|
130
|
+
gather(t)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
gather(root)
|
|
135
|
+
if (0 === maps.length) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
const decl: Record<string, Val> = {}
|
|
139
|
+
for (const m of maps) {
|
|
140
|
+
for (const k of m.aliasKeys) {
|
|
141
|
+
decl[k] = m.peg[k]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const limit: number = ctx.budget.alias
|
|
145
|
+
const size = new Map<string, number>()
|
|
146
|
+
const open = new Set<string>()
|
|
147
|
+
let over: Val | undefined = undefined
|
|
148
|
+
|
|
149
|
+
// A cycle is refused at resolution, which has not run yet, so a name
|
|
150
|
+
// already open costs nothing here rather than looping.
|
|
151
|
+
const nameSize = (key: string): number => {
|
|
152
|
+
if (size.has(key)) {
|
|
153
|
+
return size.get(key) as number
|
|
154
|
+
}
|
|
155
|
+
if (open.has(key) || !(key in decl)) {
|
|
156
|
+
return 0
|
|
157
|
+
}
|
|
158
|
+
open.add(key)
|
|
159
|
+
const n = valSize(decl[key])
|
|
160
|
+
open.delete(key)
|
|
161
|
+
size.set(key, n)
|
|
162
|
+
return n
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const valSize = (v: any): number => {
|
|
166
|
+
if (true === v.isRef) {
|
|
167
|
+
const key: string | undefined = v.aliasKey
|
|
168
|
+
return undefined === key ? 1 : 1 + nameSize(key)
|
|
169
|
+
}
|
|
170
|
+
let n = 1
|
|
171
|
+
if (true === v.isMap) {
|
|
172
|
+
for (const k of Object.keys(v.peg)) {
|
|
173
|
+
n += valSize(v.peg[k])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (Array.isArray(v.peg)) {
|
|
177
|
+
for (const e of v.peg) {
|
|
178
|
+
n += valSize(e)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (null != v.peg && true === v.peg.isVal) {
|
|
182
|
+
n += valSize(v.peg)
|
|
183
|
+
}
|
|
184
|
+
if ((true === v.isMap || true === v.isList) && null != v.spread?.cj) {
|
|
185
|
+
n += valSize(v.spread.cj)
|
|
186
|
+
}
|
|
187
|
+
return limit < n ? limit + 1 : n
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let total = 0
|
|
191
|
+
for (const m of maps) {
|
|
192
|
+
for (const k of Object.keys(m.peg)) {
|
|
193
|
+
if (!(k in decl)) {
|
|
194
|
+
total += valSize(m.peg[k])
|
|
195
|
+
if (limit < total) {
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (limit < total) {
|
|
203
|
+
over = makeNilErr(ctx, 'alias_budget', root, undefined, 'resolve')
|
|
204
|
+
; (over as any).details = { budget: '' + limit }
|
|
205
|
+
}
|
|
206
|
+
return over
|
|
207
|
+
}
|
|
8
208
|
|
|
9
209
|
|
|
10
210
|
function expandAliases(root: Val, snapmap: Map<string, Val>): void {
|
|
@@ -21,21 +221,21 @@ function expandAliases(root: Val, snapmap: Map<string, Val>): void {
|
|
|
21
221
|
seen.add(v)
|
|
22
222
|
|
|
23
223
|
if (true === v.isRef) {
|
|
24
|
-
const
|
|
25
|
-
if (undefined ===
|
|
224
|
+
const key: string | undefined = v.aliasKey
|
|
225
|
+
if (undefined === key) {
|
|
26
226
|
return
|
|
27
227
|
}
|
|
28
228
|
v.expansion = undefined
|
|
29
|
-
if (stack.includes(
|
|
229
|
+
if (stack.includes(key)) {
|
|
30
230
|
return
|
|
31
231
|
}
|
|
32
232
|
const target: Val | undefined =
|
|
33
|
-
snapmap.get(spreadSnapKey(v)) ?? (root as any).peg[
|
|
233
|
+
snapmap.get(spreadSnapKey(v)) ?? (root as any).peg[key]
|
|
34
234
|
if (null == target) {
|
|
35
235
|
return
|
|
36
236
|
}
|
|
37
237
|
v.expansion = target
|
|
38
|
-
visit(target, [...stack,
|
|
238
|
+
visit(target, [...stack, key])
|
|
39
239
|
return
|
|
40
240
|
}
|
|
41
241
|
|
|
@@ -65,9 +265,17 @@ function expandAliases(root: Val, snapmap: Map<string, Val>): void {
|
|
|
65
265
|
}
|
|
66
266
|
|
|
67
267
|
visit(root, [])
|
|
68
|
-
} /* node:coverage ignore next
|
|
268
|
+
} /* node:coverage ignore next 13 */
|
|
69
269
|
|
|
70
270
|
|
|
71
271
|
export {
|
|
272
|
+
aliasBudget,
|
|
273
|
+
aliasErrors,
|
|
274
|
+
aliasScope,
|
|
72
275
|
expandAliases,
|
|
73
276
|
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
export type {
|
|
280
|
+
AliasBinding,
|
|
281
|
+
}
|
package/src/aliasname.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// ONE PATTERN FOR THE ALIAS NAME. See docs/design/ALIASES.0.md
|
|
4
|
+
const ALIAS_NAME = '%[A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)*'
|
|
5
|
+
|
|
6
|
+
const ALIAS_RE = new RegExp('^' + ALIAS_NAME)
|
|
7
|
+
|
|
8
|
+
const ALIAS_NAME_RE = new RegExp('^' + ALIAS_NAME + '$')
|
|
9
|
+
|
|
10
|
+
// What `export` takes and a destructure heads with: `{%}` binds none.
|
|
11
|
+
const ALIAS_ITEM =
|
|
12
|
+
'(' + ALIAS_NAME + ')(?:[ \\t]*:[ \\t]*(' + ALIAS_NAME + '))?'
|
|
13
|
+
const ALIAS_SET =
|
|
14
|
+
'\\{[ \\t]*(?:%|' + ALIAS_ITEM +
|
|
15
|
+
'(?:[ \\t]*,[ \\t]*' + ALIAS_ITEM + ')*)[ \\t]*\\}'
|
|
16
|
+
const ALIAS_SET_RE = new RegExp('^' + ALIAS_SET + '$')
|
|
17
|
+
const ALIAS_ITEMS_RE = new RegExp(ALIAS_ITEM, 'g')
|
|
18
|
+
|
|
19
|
+
// THE SHORTHAND: `{ %a %b }` is `{ a: %a, b: %b }`. Names only.
|
|
20
|
+
const ALIAS_SHORTHAND =
|
|
21
|
+
'\\{\\s*' + ALIAS_NAME +
|
|
22
|
+
'(?:(?:\\s*,\\s*|\\s+)' + ALIAS_NAME + ')*\\s*\\}'
|
|
23
|
+
const ALIAS_SHORTHAND_RE = new RegExp('^' + ALIAS_SHORTHAND)
|
|
24
|
+
|
|
25
|
+
// A key carries the url of the file that declared the name.
|
|
26
|
+
const ALIAS_SCOPE = '@'
|
|
27
|
+
|
|
28
|
+
// THE ENGINE'S KEY NAMESPACE, refused to a source key.
|
|
29
|
+
const RESERVED_KEY_PREFIX = '\u0000aontu_'
|
|
30
|
+
|
|
31
|
+
// `export(...)` is read as a pair, its value under a key that changes
|
|
32
|
+
// with each declaration, so a field of that name is the document's.
|
|
33
|
+
const EXPORT_DECL_NAME = 'export'
|
|
34
|
+
const EXPORT_HOLD_KEY = RESERVED_KEY_PREFIX + 'export@'
|
|
35
|
+
let EXPORT_SEQ = 0
|
|
36
|
+
|
|
37
|
+
function exportHoldKey(): string {
|
|
38
|
+
return EXPORT_HOLD_KEY + (++EXPORT_SEQ)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isExportHoldKey(val: unknown): boolean {
|
|
42
|
+
return 'string' === typeof val && val.startsWith(EXPORT_HOLD_KEY)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
type AliasBind = { local: string, remote: string }
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function aliasScopedKey(name: string, url: string): string {
|
|
50
|
+
return name + ALIAS_SCOPE + url
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
function aliasBareName(key: string): string {
|
|
55
|
+
const at = key.indexOf(ALIAS_SCOPE)
|
|
56
|
+
return -1 === at ? key : key.substring(0, at)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
function aliasPathSegment(seg: string): string {
|
|
61
|
+
const name = aliasBareName(seg)
|
|
62
|
+
return ALIAS_NAME_RE.test(name) ? name : seg
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// Undefined where the text is not a set; the wildcard answers EMPTY.
|
|
67
|
+
function aliasSetItems(text: string): AliasBind[] | undefined {
|
|
68
|
+
if (!ALIAS_SET_RE.test(text)) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
return Array.from(text.matchAll(ALIAS_ITEMS_RE),
|
|
72
|
+
(m) => ({ local: m[1], remote: m[2] ?? m[1] }))
|
|
73
|
+
} /* node:coverage ignore next 22 */
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
ALIAS_NAME,
|
|
78
|
+
ALIAS_RE,
|
|
79
|
+
ALIAS_NAME_RE,
|
|
80
|
+
ALIAS_SET,
|
|
81
|
+
ALIAS_SHORTHAND_RE,
|
|
82
|
+
EXPORT_DECL_NAME,
|
|
83
|
+
RESERVED_KEY_PREFIX,
|
|
84
|
+
exportHoldKey,
|
|
85
|
+
isExportHoldKey,
|
|
86
|
+
aliasScopedKey,
|
|
87
|
+
aliasBareName,
|
|
88
|
+
aliasPathSegment,
|
|
89
|
+
aliasSetItems,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export type {
|
|
94
|
+
AliasBind,
|
|
95
|
+
}
|
package/src/aontu.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type {
|
|
|
28
28
|
} from './allow'
|
|
29
29
|
import { graphOf } from './graph'
|
|
30
30
|
import { relationCheck, relationErrors } from './relation'
|
|
31
|
+
import { aliasBudget, aliasErrors } from './alias'
|
|
31
32
|
import { view, viewSet, viewTree } from './view'
|
|
32
33
|
import { loadProfile } from './profile'
|
|
33
34
|
import { desugarTemplate, resugarTemplate, markerFor } from './template'
|
|
@@ -35,7 +36,7 @@ import { format, unifiedDiff } from './format'
|
|
|
35
36
|
export type { LintFinding, FormatReport, FormatOptions } from './format'
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
const VERSION = '0.
|
|
39
|
+
const VERSION = '0.69.0'
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
function genQuiet(val: any, aontu: Aontu): any {
|
|
@@ -153,16 +154,28 @@ class Aontu {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
if (null != pval && 0 === errs.length) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
// T-1: EXPANDED SIZE IS CHARGED BEFORE EVALUATION, here rather
|
|
158
|
+
// than in generate, because an editor unifies on each keystroke
|
|
159
|
+
// and a document too big to evaluate must be turned away there
|
|
160
|
+
// too.
|
|
161
|
+
const over = aliasBudget(ac as any, pval)
|
|
162
|
+
|
|
163
|
+
if (undefined !== over) {
|
|
164
|
+
out = over
|
|
165
|
+
errs = [over]
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
let uni = new Unify(pval, this.lang, ac, src)
|
|
169
|
+
errs = uni.err
|
|
170
|
+
|
|
171
|
+
// Never nullish: Unify.res starts as the root Val, unite() returns a
|
|
172
|
+
// Val on every arm, and its catch-all turns a throwing node into an
|
|
173
|
+
// 'internal' NilVal.
|
|
174
|
+
out = uni.res
|
|
175
|
+
out.graph = graphOf(out)
|
|
176
|
+
}
|
|
163
177
|
|
|
164
178
|
out.deps = pval.deps
|
|
165
|
-
out.graph = graphOf(out)
|
|
166
179
|
out.err = errs
|
|
167
180
|
ac.root = out
|
|
168
181
|
}
|
|
@@ -193,6 +206,7 @@ class Aontu {
|
|
|
193
206
|
: uval.gen(ac as any)
|
|
194
207
|
|
|
195
208
|
if (!uval.isNil && 0 === ac.err.length) {
|
|
209
|
+
aliasErrors(ac as any, uval)
|
|
196
210
|
relationErrors(ac as any, uval)
|
|
197
211
|
if (0 < ac.err.length) {
|
|
198
212
|
out = undefined
|
package/src/aontumodel.ts
CHANGED
|
@@ -10,7 +10,7 @@ const AONTU_SCHEME = 'aontu:'
|
|
|
10
10
|
const AONTU_SOURCES: Record<string, string> = {
|
|
11
11
|
"aontu:lang/markdown": "# aontu:lang/markdown --- THE MARKDOWN PROFILE. The text profile plus\n# markdown's own two: the HTML comment form, and the template marker\n# its files carry.\n\n@\"aontu:profile\"\n\naontu: Lang: lang: \"markdown\"\naontu: Lang: indent: { unit:\" \" width:2 }\naontu: Lang: comment: block: { open:\"<!--\" close:\"-->\" }\naontu: Lang: template: { marker:\"<!---\" ext: [\"md\" \"markdown\"] }\n",
|
|
12
12
|
"aontu:lang/text": "# aontu:lang/text --- THE TEXT PROFILE. Indentation and nothing else.\n# Every language without one of its own is read under it.\n\n@\"aontu:profile\"\n\naontu: Lang: { lang:\"text\" indent: { unit:\" \" width:2 } }\n",
|
|
13
|
-
"aontu:profile": "# aontu:profile --- THE PROFILE VOCABULARY. A language declared as\n# data: its name, its indentation, its comment forms and the marker its\n# generators carry. `aontu template` and `aontu fmt` read one through\n# --profile. EXPERIMENTAL, versioned by canon-hash later.\n\n%comment = close({ open?:string prefix?:string close?:string })\n\n%profile = close({\n lang: string & length(min(1))\n indent: close({\n unit: *\" \"|string & length(min(1))\n width: *2|integer & min(0) & max(16)\n })\n comment?: close({ line?:%comment block?:%comment doc?:%comment })\n template?: close({\n marker: string & length(min(1))\n close?: string & length(min(1))\n ext?: [&: string & re(\"^[A-Za-z0-9_+-]+$\")]\n })\n})\n\n# Profile names the schema; Lang is where one lands. type() so naming\n# the schema neither generates it nor asks a document to fill it.\naontu: { Profile:type(%profile) Lang:%profile }\n",
|
|
13
|
+
"aontu:profile": "# aontu:profile --- THE PROFILE VOCABULARY. A language declared as\n# data: its name, its indentation, its comment forms and the marker its\n# generators carry. `aontu template` and `aontu fmt` read one through\n# --profile. EXPERIMENTAL, versioned by canon-hash later.\n\n%comment = close({ open?:string prefix?:string close?:string })\n\n%profile = close({\n lang: string & length(min(1))\n indent: close({\n unit: *\" \"|string & length(min(1))\n width: *2|integer & min(0) & max(16)\n })\n comment?: close({ line?:%comment block?:%comment doc?:%comment })\n template?: close({\n marker: string & length(min(1))\n close?: string & length(min(1))\n ext?: [&: string & re(\"^[A-Za-z0-9_+-]+$\")]\n })\n})\n\n# Published: a name crosses only to a document that asks for it.\nexport({ %profile, %comment })\n\n# Profile names the schema; Lang is where one lands. type() so naming\n# the schema neither generates it nor asks a document to fill it.\naontu: { Profile:type(%profile) Lang:%profile }\n",
|
|
14
14
|
"aontu:system": "# aontu:system --- the SYSTEM VOCABULARY. EXPERIMENTAL until the\n# distribution layer can version it by canon-hash.\n\naontu: System: {\n Port: type({ direction: *in|out|inout protocol?:string })\n\n Component: type({ ports?: { &: $.aontu.System.Port } })\n\n # Written out rather than Component & { kind: service }: a reference\n # to a type()-marked member does not survive the include.\n Service: type({ kind:service ports?: { &: $.aontu.System.Port } })\n\n # A grammar, not re(): the identifier shape is a quantified group\n # holding a quantifier, which re() refuses. hide()n so a schema's\n # grammar does not generate into the document it checks.\n semverPreRelease: hide(\n abnf(\n `pre-release = pre-release-id *( \".\" pre-release-id )\npre-release-id = \"0\" [ *digit alnum-tail ]\n / positive-digit *digit [ alnum-tail ] / alnum-tail\nalnum-tail = non-digit *id-char\nid-char = digit / non-digit\nnon-digit = letter / \"-\"\ndigit = \"0\" / positive-digit\npositive-digit = %x31-39\nletter = %x41-5A / %x61-7A\n`\n )\n )\n semverBuild: hide(\n abnf(\n `build = build-id *( \".\" build-id )\nbuild-id = 1*id-char\nid-char = digit / non-digit\nnon-digit = letter / \"-\"\ndigit = \"0\" / positive-digit\npositive-digit = %x31-39\nletter = %x41-5A / %x61-7A\n`\n )\n )\n\n # major minor patch pre-release build, tail defaulted.\n Semver: type(\n [\n integer & min(0)\n *0|(integer & min(0))\n *0|(integer & min(0))\n *\"\"|parse($.aontu.System.semverPreRelease)\n *\"\"|parse($.aontu.System.semverBuild)\n ] & length(5)\n )\n}\n",
|
|
15
15
|
"aontu:view": "# aontu:view --- the FIGURE VOCABULARY. EXPERIMENTAL until the\n# distribution layer can version it by canon-hash.\n\naontu: View: Figure: type({\n kind: doc|lattice|tree|matrix|graph|layer|sets|layers\n | ladder\n out: string\n\n # Every kind.\n as?: text|mermaid|dot|er|svg\n at?: string\n maxRows?: integer & min(0)\n\n # doc.\n depth?: integer & min(0)\n\n # tree, matrix, layer, graph.\n relation?: string\n relations?: [&: string]\n roots?: [&: string]\n\n # matrix.\n order?: canon|partition\n closure?: boolean\n\n # graph, layer.\n groupBy?: string\n label?: string\n layers?: [&: string]\n edges?: upward|all|none\n\n # sets, layers.\n sets?: string\n member?: string\n universe?: string\n minDegree?: integer & min(0)\n maxCols?: integer & min(0)\n minSize?: integer & min(0)\n})\n"
|
|
16
16
|
}
|
package/src/ctx.ts
CHANGED
|
@@ -93,7 +93,9 @@ class AontuContext {
|
|
|
93
93
|
|
|
94
94
|
_fixroot: any
|
|
95
95
|
|
|
96
|
-
budget: {
|
|
96
|
+
budget: {
|
|
97
|
+
passes: number, revisits: number, depth: number, alias: number
|
|
98
|
+
}
|
|
97
99
|
|
|
98
100
|
// The include manifest sink (G5, docs/trust.md): every include the
|
|
99
101
|
// resolver reads is recorded here as { path, capability }, and
|
|
@@ -153,6 +155,7 @@ class AontuContext {
|
|
|
153
155
|
passes: budget.passes ?? 9,
|
|
154
156
|
revisits: 999,
|
|
155
157
|
depth: budget.depth ?? 1000,
|
|
158
|
+
alias: budget.alias ?? 1000000,
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
|
package/src/err.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright (c) 2021-2025 Richard Rodger, MIT License */
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import { aliasPathSegment } from './aliasname'
|
|
4
5
|
import { sep } from 'node:path'
|
|
5
6
|
|
|
6
7
|
import { util } from '@tabnas/jsonic'
|
|
@@ -71,7 +72,10 @@ function descErr<NILS extends NilVal | NilVal[]>(
|
|
|
71
72
|
let v1src = resolveSrc(v1, errctx)
|
|
72
73
|
let v2src = resolveSrc(v2, errctx)
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// A list index is a number here, and only a key can be an alias.
|
|
76
|
+
let path = ['$', ...err.path.map((p: any) =>
|
|
77
|
+
'string' === typeof p ? aliasPathSegment(p) : p)]
|
|
78
|
+
.filter((p: any) => null != p && '' !== p)
|
|
75
79
|
|
|
76
80
|
// '$' is neither null nor '', so the filter always leaves it.
|
|
77
81
|
let valpath = path.join('.')
|
|
@@ -125,6 +129,20 @@ function descErr<NILS extends NilVal | NilVal[]>(
|
|
|
125
129
|
col: v2.site.col,
|
|
126
130
|
})),
|
|
127
131
|
|
|
132
|
+
// A NAME IS WHERE THE VALUE ENTERED THIS PATH, which is not
|
|
133
|
+
// where it is written (ALIASES.0.md A-1).
|
|
134
|
+
...[v1, v2].map((v: any) => null != v?.site?.via && errmsg({
|
|
135
|
+
color: { active: colorActive(), line: '\x1b[34m' },
|
|
136
|
+
txts: {
|
|
137
|
+
msg: 'Value arrived through ' + v.site.via.name,
|
|
138
|
+
site: ''
|
|
139
|
+
},
|
|
140
|
+
smsg: 'used ' + v.site.via.name + ' here',
|
|
141
|
+
file: resolveFile(v.site.via.url),
|
|
142
|
+
src: resolveSrc({ site: v.site.via } as any, errctx),
|
|
143
|
+
row: v.site.via.row,
|
|
144
|
+
col: v.site.via.col,
|
|
145
|
+
})),
|
|
128
146
|
|
|
129
147
|
]
|
|
130
148
|
.filter((n: any) => null != n && false !== n)
|
package/src/format.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { failureFinding } from './vet'
|
|
|
6
6
|
import type { VetFinding } from './vet'
|
|
7
7
|
import type { Resolver } from './type'
|
|
8
8
|
import { desugarTemplate, resugarTemplate, templateOutputs } from './template'
|
|
9
|
+
import { ALIAS_RE, EXPORT_DECL_NAME, isExportHoldKey } from './aliasname'
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const BUDGET = 80
|
|
@@ -303,6 +304,19 @@ class Reader {
|
|
|
303
304
|
this.i += 2
|
|
304
305
|
return { t: 'spread', value: this.value(), at }
|
|
305
306
|
}
|
|
307
|
+
// `export({ %a })` is ONE declaration lexed as a pair.
|
|
308
|
+
if (isExportHoldKey(this.T[this.i].val) &&
|
|
309
|
+
EXPORT_DECL_NAME === this.T[this.i].src && this.atKey()) {
|
|
310
|
+
const text = EXPORT_DECL_NAME + '(' + this.T[this.i + 2].src + ')'
|
|
311
|
+
this.i += 3
|
|
312
|
+
return { t: 'atom', text, at }
|
|
313
|
+
}
|
|
314
|
+
if (this.atKey() && ALIAS_RE.test('' + this.T[this.i].src) &&
|
|
315
|
+
this.T[this.i].src === this.T[this.i + 2]?.src) {
|
|
316
|
+
const text = '' + this.T[this.i].src
|
|
317
|
+
this.i += 3
|
|
318
|
+
return { t: 'atom', text, at }
|
|
319
|
+
}
|
|
306
320
|
if (this.atKey()) {
|
|
307
321
|
const tok = this.T[this.i]
|
|
308
322
|
const opt = '#QM' === this.name(1)
|
package/src/hints.ts
CHANGED
|
@@ -189,6 +189,8 @@ const hints: Record<string, string> = {
|
|
|
189
189
|
|
|
190
190
|
elided_value: 'A key or element was written with no value after the colon. An\nelided value is a mistake in the source rather than a null: write\n`null` if that is what was meant, or supply the value.\n \nExamples:\n a:null -> null # An explicit null, which is a value;\n a: -> nil # ... but nothing at all is not;\n a: b:1 -> {..} # A colon chain is not an elision;\n [1,] -> [1] # ... nor is a trailing comma.',
|
|
191
191
|
|
|
192
|
+
alias_budget: 'Alias expansion is bounded but not small: a name that names names\nexpands to the product of what they hold, so twenty shallow\ndeclarations reach a million nodes. The expanded size is counted\nbefore evaluation and refused over the budget ({budget} nodes), so\nthe document is turned away rather than run until memory is gone.\n \nExpansion terminates whatever the budget: an alias takes no\nparameters, a cycle is refused, and a file declares finitely many\nnames. The budget is about SIZE, not about termination.\n \nRaise it with trust.budget.alias where the document is trusted and\nthe machine can hold the result.',
|
|
193
|
+
reserved_key: 'The \\u0000aontu_ key prefix belongs to the engine. A document\'s key\norder, its spreads and its alias declarations are held under keys in\nthat namespace, so a source key written there would land on one of\nthem. It is refused where it stands rather than silently replacing\nwhat the parser put there. No ordinary key needs the prefix: it\nbegins with a NUL, which only an escape can spell.\n \nExamples:\n "\\u0000aontu_order": 1 -> nil # The namespace is the engine\'s;\n "aontu_order": 1 -> {..} # ... without the NUL it is a key;\n "___merge": 1 -> {..} # ... and so is this.',
|
|
192
194
|
alias_colon: 'An alias is declared with `=`: write `%name = value`. Until 0.57.0\nthe declaration was spelled with a colon, `%name: value`, and that\nform is refused rather than read as an ordinary key -- a document\nwritten for the old spelling fails here, at the declaration, instead\nof gaining a key named `%name` and a use that resolves to nothing.\n \nExamples:\n %u8 = integer & min(0) -> {..} # Declares %u8, which a: %u8 uses;\n %u8: integer -> nil # The form before 0.58.0, refused;\n "%u8": 1 -> {..} # A quoted key is an ordinary key.',
|
|
193
195
|
|
|
194
196
|
bare_punct: 'A bare string holds letters, digits, `-` and `_`, and nothing else.\nThis one holds `{char}`, in `{text}`. Every other punctuation\ncharacter is either syntax or an error, never silently part of a\nstring: a value that needs one is written quoted, and a `>` or `<`\nthat was meant as a bound is written as min(x), max(x), above(x) or\nbelow(x).\n \nExamples:\n a: team-payments -> "team-payments" # `-` and `_` are text;\n a: 2026-09-05 -> "2026-09-05" # ... digits included;\n a: x=y -> nil # `=` is not;\n a: "x=y" -> "x=y" # ... so quote it;\n a: >10 -> nil # Not an operator: write above(10).',
|
|
@@ -591,13 +593,40 @@ const hints: Record<string, string> = {
|
|
|
591
593
|
' listen: $.%port -> nil # Not a path segment.',
|
|
592
594
|
|
|
593
595
|
'alias_not_toplevel':
|
|
594
|
-
'An alias declaration sits at the root of the
|
|
595
|
-
'`x: { %a = 1 }` is refused because `%a` resolves
|
|
596
|
-
'declaration would be erased from the output
|
|
597
|
-
'and still unreachable by any reference, not
|
|
598
|
-
'Where the declaration LANDS decides this, not
|
|
599
|
-
'so an include spliced at the root may declare
|
|
600
|
-
'taken as a value may not
|
|
596
|
+
'An alias declaration written as a KEY sits at the root of the\n' +
|
|
597
|
+
'document. A nested `x: { %a = 1 }` is refused because `%a` resolves\n' +
|
|
598
|
+
'from the root: the declaration would be erased from the output,\n' +
|
|
599
|
+
'being a declaration, and still unreachable by any reference, not\n' +
|
|
600
|
+
'being at the root. Where the declaration LANDS decides this, not\n' +
|
|
601
|
+
'where it was written, so an include spliced at the root may declare\n' +
|
|
602
|
+
'one and an include taken as a value may not.\n' +
|
|
603
|
+
'To name a shape where it is used, write the declaration as a VALUE\n' +
|
|
604
|
+
'prefix instead: `x: %a = 1` is accepted at any depth, leaves the\n' +
|
|
605
|
+
'value alone, and declares `%a` for the document.',
|
|
606
|
+
|
|
607
|
+
'export_arg':
|
|
608
|
+
'`export` takes a SET OF ALIAS NAMES and nothing else: write\n' +
|
|
609
|
+
'`export({ %a, %b })`. A key already crosses a file boundary as a\n' +
|
|
610
|
+
'value, so a bare word names nothing `export` could publish, and a\n' +
|
|
611
|
+
'single name still stands in a set. The `{%}` wildcard is the\n' +
|
|
612
|
+
'importing side\'s: the publishing file chooses what it publishes.\n' +
|
|
613
|
+
' \nExamples:\n' +
|
|
614
|
+
' %u8 = integer\n' +
|
|
615
|
+
' export({ %u8 }) -> published;\n' +
|
|
616
|
+
' export({ u8 }) -> nil # A key, not an alias;\n' +
|
|
617
|
+
' export(%u8) -> nil # A set, even of one;\n' +
|
|
618
|
+
' export({%}) -> nil # The wildcard is the importer\'s.',
|
|
619
|
+
|
|
620
|
+
'import_not_exported':
|
|
621
|
+
'The file this name was asked of does not publish `{name}`. A name\n' +
|
|
622
|
+
'belongs to the file that declares it and crosses only where that\n' +
|
|
623
|
+
'file says so, which is what `export` is for: add the name to the\n' +
|
|
624
|
+
'other file\'s `export({ ... })`, or write the value in this one.\n' +
|
|
625
|
+
'The include still placed the file\'s values -- it is the NAME that\n' +
|
|
626
|
+
'did not cross.\n' +
|
|
627
|
+
' \nExamples:\n' +
|
|
628
|
+
' { %u8 } = @"types.aon" -> bound, if types.aon exports %u8;\n' +
|
|
629
|
+
' { %secret } = @"types.aon" -> nil # ... and refused if not.',
|
|
601
630
|
|
|
602
631
|
'patch_assignment':
|
|
603
632
|
'This is not a <path>=<value> assignment. The path is what stands\n' +
|
|
@@ -882,7 +911,11 @@ const codeClasses: Record<string, string> = {
|
|
|
882
911
|
pref_implicit_bag: 'parse',
|
|
883
912
|
alias_not_toplevel: 'parse',
|
|
884
913
|
alias_in_path: 'parse',
|
|
914
|
+
alias_budget: 'budget',
|
|
915
|
+
reserved_key: 'parse',
|
|
885
916
|
alias_colon: 'parse',
|
|
917
|
+
export_arg: 'parse',
|
|
918
|
+
import_not_exported: 'reference',
|
|
886
919
|
bare_punct: 'parse',
|
|
887
920
|
not_number: 'parse',
|
|
888
921
|
negative: 'parse',
|