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.
Files changed (74) hide show
  1. package/dist/alias.d.ts +12 -1
  2. package/dist/alias.js +176 -6
  3. package/dist/alias.js.map +1 -1
  4. package/dist/aliasname.d.ts +19 -0
  5. package/dist/aliasname.js +65 -0
  6. package/dist/aliasname.js.map +1 -0
  7. package/dist/aontu.d.ts +1 -1
  8. package/dist/aontu.js +21 -8
  9. package/dist/aontu.js.map +1 -1
  10. package/dist/aontumodel.js +1 -1
  11. package/dist/aontumodel.js.map +1 -1
  12. package/dist/ctx.d.ts +1 -0
  13. package/dist/ctx.js +1 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/err.js +18 -1
  16. package/dist/err.js.map +1 -1
  17. package/dist/format.js +14 -0
  18. package/dist/format.js.map +1 -1
  19. package/dist/hints.js +36 -7
  20. package/dist/hints.js.map +1 -1
  21. package/dist/lang.js +289 -35
  22. package/dist/lang.js.map +1 -1
  23. package/dist/lsp.d.ts +10 -4
  24. package/dist/lsp.js +116 -7
  25. package/dist/lsp.js.map +1 -1
  26. package/dist/site.d.ts +9 -0
  27. package/dist/site.js +2 -1
  28. package/dist/site.js.map +1 -1
  29. package/dist/subsume.js +6 -1
  30. package/dist/subsume.js.map +1 -1
  31. package/dist/val/BagVal.d.ts +1 -0
  32. package/dist/val/BagVal.js +3 -0
  33. package/dist/val/BagVal.js.map +1 -1
  34. package/dist/val/EmitFuncVal.js +2 -1
  35. package/dist/val/EmitFuncVal.js.map +1 -1
  36. package/dist/val/FilterFuncVal.js +1 -1
  37. package/dist/val/FilterFuncVal.js.map +1 -1
  38. package/dist/val/FuncBaseVal.d.ts +2 -1
  39. package/dist/val/FuncBaseVal.js +25 -1
  40. package/dist/val/FuncBaseVal.js.map +1 -1
  41. package/dist/val/MapVal.js +10 -4
  42. package/dist/val/MapVal.js.map +1 -1
  43. package/dist/val/MatchFuncVal.js +2 -1
  44. package/dist/val/MatchFuncVal.js.map +1 -1
  45. package/dist/val/NilVal.js +3 -0
  46. package/dist/val/NilVal.js.map +1 -1
  47. package/dist/val/RecurseVal.d.ts +1 -0
  48. package/dist/val/RecurseVal.js +7 -3
  49. package/dist/val/RecurseVal.js.map +1 -1
  50. package/dist/val/RefVal.d.ts +1 -0
  51. package/dist/val/RefVal.js +29 -12
  52. package/dist/val/RefVal.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/alias.ts +214 -6
  55. package/src/aliasname.ts +95 -0
  56. package/src/aontu.ts +23 -9
  57. package/src/aontumodel.ts +1 -1
  58. package/src/ctx.ts +4 -1
  59. package/src/err.ts +19 -1
  60. package/src/format.ts +14 -0
  61. package/src/hints.ts +40 -7
  62. package/src/lang.ts +334 -30
  63. package/src/lsp.ts +136 -6
  64. package/src/site.ts +15 -1
  65. package/src/subsume.ts +9 -2
  66. package/src/val/BagVal.ts +4 -0
  67. package/src/val/EmitFuncVal.ts +3 -2
  68. package/src/val/FilterFuncVal.ts +2 -2
  69. package/src/val/FuncBaseVal.ts +30 -1
  70. package/src/val/MapVal.ts +10 -4
  71. package/src/val/MatchFuncVal.ts +3 -2
  72. package/src/val/NilVal.ts +3 -0
  73. package/src/val/RecurseVal.ts +8 -3
  74. 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 name: string | undefined = v.aliasName
25
- if (undefined === name) {
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(name)) {
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[name]
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, name])
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 5 */
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
+ }
@@ -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.68.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
- let uni = new Unify(pval, this.lang, ac, src)
157
- errs = uni.err
158
-
159
- // Never nullish: Unify.res starts as the root Val, unite() returns a
160
- // Val on every arm, and its catch-all turns a throwing node into an
161
- // 'internal' NilVal.
162
- out = uni.res
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: { passes: number, revisits: number, depth: number }
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
- let path = ['$', ...err.path].filter((p: any) => null != p && '' !== p)
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 document. A nested\n' +
595
- '`x: { %a = 1 }` is refused because `%a` resolves from the root: the\n' +
596
- 'declaration would be erased from the output, being a declaration,\n' +
597
- 'and still unreachable by any reference, not being at the root.\n' +
598
- 'Where the declaration LANDS decides this, not where it was written,\n' +
599
- 'so an include spliced at the root may declare one and an include\n' +
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',