aontu 0.60.0 → 0.62.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 (113) hide show
  1. package/README.md +3 -3
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +7 -1
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/allow.d.ts +23 -0
  6. package/dist/allow.js +230 -0
  7. package/dist/allow.js.map +1 -0
  8. package/dist/aontu.d.ts +4 -2
  9. package/dist/aontu.js +4 -2
  10. package/dist/aontu.js.map +1 -1
  11. package/dist/cli.d.ts +10 -1
  12. package/dist/cli.js +1000 -47
  13. package/dist/cli.js.map +1 -1
  14. package/dist/format.d.ts +1 -0
  15. package/dist/format.js +154 -12
  16. package/dist/format.js.map +1 -1
  17. package/dist/helpdoc.d.ts +16 -0
  18. package/dist/helpdoc.js +59 -0
  19. package/dist/helpdoc.js.map +1 -0
  20. package/dist/hints.js +15 -2
  21. package/dist/hints.js.map +1 -1
  22. package/dist/lang.js +93 -40
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lower.d.ts +3 -0
  25. package/dist/lower.js +3 -0
  26. package/dist/lower.js.map +1 -1
  27. package/dist/lsp.d.ts +1 -1
  28. package/dist/lsp.js +1 -1
  29. package/dist/lsp.js.map +1 -1
  30. package/dist/relation.d.ts +2 -0
  31. package/dist/relation.js +7 -1
  32. package/dist/relation.js.map +1 -1
  33. package/dist/render.js +26 -21
  34. package/dist/render.js.map +1 -1
  35. package/dist/sigdecl.js +1 -1
  36. package/dist/sigdecl.js.map +1 -1
  37. package/dist/std.js +112 -69
  38. package/dist/std.js.map +1 -1
  39. package/dist/template.d.ts +2 -1
  40. package/dist/template.js +51 -15
  41. package/dist/template.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/dist/val/AggFuncVal.js +2 -2
  44. package/dist/val/AggFuncVal.js.map +1 -1
  45. package/dist/val/CmpFuncVal.d.ts +20 -0
  46. package/dist/val/CmpFuncVal.js +245 -0
  47. package/dist/val/CmpFuncVal.js.map +1 -0
  48. package/dist/val/EachFuncVal.d.ts +1 -2
  49. package/dist/val/EachFuncVal.js +15 -29
  50. package/dist/val/EachFuncVal.js.map +1 -1
  51. package/dist/val/FormFuncVal.js.map +1 -1
  52. package/dist/val/LowerFuncVal.js +17 -1
  53. package/dist/val/LowerFuncVal.js.map +1 -1
  54. package/dist/val/NamerFuncVal.d.ts +12 -0
  55. package/dist/val/NamerFuncVal.js +176 -0
  56. package/dist/val/NamerFuncVal.js.map +1 -0
  57. package/dist/val/NilVal.js +29 -3
  58. package/dist/val/NilVal.js.map +1 -1
  59. package/dist/val/NomFuncVal.d.ts +12 -0
  60. package/dist/val/NomFuncVal.js +187 -0
  61. package/dist/val/NomFuncVal.js.map +1 -0
  62. package/dist/val/PackFuncVal.js +3 -3
  63. package/dist/val/PackFuncVal.js.map +1 -1
  64. package/dist/val/RefVal.js +1 -1
  65. package/dist/val/TranslateFuncVal.d.ts +12 -0
  66. package/dist/val/TranslateFuncVal.js +101 -0
  67. package/dist/val/TranslateFuncVal.js.map +1 -0
  68. package/dist/val/UpperFuncVal.js +17 -1
  69. package/dist/val/UpperFuncVal.js.map +1 -1
  70. package/dist/val/caserange.d.ts +3 -0
  71. package/dist/val/caserange.js +110 -0
  72. package/dist/val/caserange.js.map +1 -0
  73. package/dist/vet.d.ts +12 -0
  74. package/dist/vet.js +209 -1
  75. package/dist/vet.js.map +1 -1
  76. package/grammar/aontu.abnf +1 -1
  77. package/grammar/aontu.gbnf +1 -1
  78. package/grammar/aontu.lark +1 -1
  79. package/grammar/aontu.tmLanguage.json +1 -1
  80. package/package.json +1 -1
  81. package/skill/SKILL.md +8 -0
  82. package/skill/init/check.sh +28 -0
  83. package/skill/init/data.aon +12 -0
  84. package/skill/init/model.aon +19 -0
  85. package/skill/tasks.md +151 -0
  86. package/src/agentsmd.ts +13 -2
  87. package/src/allow.ts +316 -0
  88. package/src/aontu.ts +10 -1
  89. package/src/cli.ts +1131 -53
  90. package/src/format.ts +191 -14
  91. package/src/helpdoc.ts +77 -0
  92. package/src/hints.ts +16 -2
  93. package/src/lang.ts +98 -42
  94. package/src/lower.ts +3 -3
  95. package/src/lsp.ts +1 -1
  96. package/src/relation.ts +21 -1
  97. package/src/render.ts +26 -21
  98. package/src/sigdecl.ts +1 -1
  99. package/src/std.ts +112 -69
  100. package/src/template.ts +55 -15
  101. package/src/val/AggFuncVal.ts +2 -2
  102. package/src/val/CmpFuncVal.ts +411 -0
  103. package/src/val/EachFuncVal.ts +49 -50
  104. package/src/val/LowerFuncVal.ts +18 -1
  105. package/src/val/NilVal.ts +29 -3
  106. package/src/val/NomFuncVal.ts +287 -0
  107. package/src/val/PackFuncVal.ts +3 -3
  108. package/src/val/RefVal.ts +1 -1
  109. package/src/val/TranslateFuncVal.ts +182 -0
  110. package/src/val/UpperFuncVal.ts +18 -1
  111. package/src/val/caserange.ts +115 -0
  112. package/src/vet.ts +287 -1
  113. package/src/val/FormFuncVal.ts +0 -119
@@ -0,0 +1,411 @@
1
+ /* Copyright (c) 2026 Richard Rodger, MIT License */
2
+
3
+ // THE COMPONENT PRIMITIVES -- SPIKE (docs/design/JOSTRACA.0.md).
4
+ //
5
+ // `Folder`, `File` and `Content` are the first three of jostraca's
6
+ // component set, spelled as aontu functions and keeping jostraca's
7
+ // own capitalisation. They are the authoring surface an aontu
8
+ // document uses to say what FILES it produces:
9
+ //
10
+ // out: folder("src", [
11
+ // file("main.ts", [
12
+ // content("export const x = 1")
13
+ // ])
14
+ // ])
15
+ //
16
+ // WHAT THIS REPLACES. `aontu:code` (ts/src/std.ts, test/spec/
17
+ // aontu-code.tsv) is a hand-written vocabulary an author fills in as
18
+ // DATA -- `aontu: Code: units: [{path, lang, decls}]` -- and `aontu
19
+ // render` folds that instance into bytes. The instance is checked by
20
+ // unification against the vocabulary, which is the good half; the bad
21
+ // half is that an author writes a schema instance rather than a
22
+ // generator, the vocabulary owns every construct the renderer will
23
+ // ever emit, and nothing in it corresponds to a component someone can
24
+ // already write in jostraca. These primitives take the other road:
25
+ // the CALL is the construct, the call's own resolve is where the
26
+ // shape is refused, and the value it lands is a jostraca component
27
+ // tree that jostraca's build phase already knows how to walk.
28
+ //
29
+ // THE VALUE IS AN ORDINARY MAP, which is the whole reason this can be
30
+ // a spike at all. A component call resolves to
31
+ //
32
+ // {cmp: "Folder", props: {name: "src"}, children: [...]}
33
+ //
34
+ // so nothing downstream needs new machinery: `generate()` emits the
35
+ // tree as JSON, `canon` renders the calls back, references and
36
+ // spreads reach into a tree as they reach into any map, and `each()`
37
+ // over model data drops straight into a `children` list -- which is
38
+ // the point, and is why the second argument must BE a list rather
39
+ // than accepting a bare node as a convenience. `each($.fields,
40
+ // content(...))` is already the list; a one-child special case would
41
+ // make `file(n, each(...))` and `file(n, [each(...)])` both legal and
42
+ // different.
43
+ //
44
+ // CHILDREN FLATTEN, for the same reason. A generator's output lands
45
+ // in the MIDDLE of a written list --
46
+ //
47
+ // file("planet.ts", [
48
+ // content("export interface Planet {"),
49
+ // each($.fields, content(" " + _ + ": string")),
50
+ // content("}")
51
+ // ])
52
+ //
53
+ // -- and that list therefore holds two nodes and a LIST OF NODES. A
54
+ // list is not a node and could only be an error otherwise, so
55
+ // splicing is the total rule and nesting is not: children are
56
+ // flattened to any depth before the containment grammar is checked.
57
+ // This is the finding the spike's worked example produced; every
58
+ // component system that takes children arrives at it.
59
+ //
60
+ // THE NODE MAP IS CLOSED and its `props` map is not. The three keys
61
+ // are the vocabulary and a fourth is a mistake, so a typo in
62
+ // `folder(...) & {childrn: []}` is refused where every other mistake
63
+ // in an aontu document is refused. Props are the COMPONENT's business
64
+ // -- jostraca's File already reads `mode` and `exclude`, and a
65
+ // component the spike does not implement will read its own -- so they
66
+ // pass through, and only the one prop each primitive cannot work
67
+ // without is checked here.
68
+ //
69
+ // THE CONTAINMENT GRAMMAR IS CHECKED AT THE CALL. A Folder holds
70
+ // folders and files, a File holds content, and Content holds nothing.
71
+ // Unification cannot state that on its own -- a node is a map, and
72
+ // every node map unifies with every other -- so the constructor
73
+ // states it, at the site the author wrote, which is the argument for
74
+ // making these functions rather than another vocabulary.
75
+ //
76
+ // NOT STAGED, deliberately. A component call answers from its
77
+ // arguments alone: it has no dependence on WHERE it sits (unlike
78
+ // `key()`) and it reads no bag that a sibling may still merge into
79
+ // (unlike `pack`/`each`). A children list BUILT by one of
80
+ // those combinators still waits, because the combinator residuates
81
+ // and the enclosing call is not `pegdone` until it fires -- so the
82
+ // ordinary args-done gate is the correct one and the staging rule
83
+ // costs nothing here.
84
+ //
85
+ // SPIKE SCOPE. TypeScript only; no Go port yet, and so:
86
+ // deliberately absent from `test/spec/signature.tsv`, from
87
+ // `BUILTIN_FUNCS` (ts/src/lsp.ts) and from the `grammar/` files, all
88
+ // three of which are pinned in cross-port parity and would go red on
89
+ // a TS-only change. Argument arity and shape are therefore refused
90
+ // HERE rather than by the parse-time arity table or the signature
91
+ // gate, both of which read the declaration this spike does not have.
92
+ // Failures are `invalid-arg` for the same reason -- a new code needs
93
+ // a row in test/spec/errcodes.tsv and an entry in BOTH ports' code
94
+ // tables (AGENTS.md, "The shared test suite").
95
+
96
+ import type {
97
+ Val,
98
+ ValSpec,
99
+ } from '../type'
100
+
101
+ import {
102
+ AontuContext,
103
+ } from '../ctx'
104
+
105
+ import { makeNilErr } from '../err'
106
+
107
+ import { MapVal } from './MapVal'
108
+ import { ListVal } from './ListVal'
109
+ import { StringVal } from './StringVal'
110
+ import { FuncBaseVal } from './FuncBaseVal'
111
+
112
+
113
+ // The component names this spike serves, each with the children it
114
+ // admits and the one prop it cannot work without. Adding a fourth
115
+ // primitive is an entry here plus the two-line subclass below.
116
+ type CmpDef = {
117
+ // The jostraca component this call builds. CAPITALISED, because it
118
+ // names a component in the other project's exported set and the
119
+ // bridge looks it up by that name -- while the aontu FUNCTION is
120
+ // lower case, like every other builtin in this language. The two
121
+ // spellings say what they are: `file(...)` is aontu, `"File"` is
122
+ // jostraca's.
123
+ cmp: string
124
+ // The children this component admits, by aontu function name. Empty
125
+ // means a leaf: any child at all is a mistake.
126
+ children: string[]
127
+ // The prop a bare string argument fills. ABSENT means the component
128
+ // has no one-string spelling and its spec must be a map: `listitems`
129
+ // is driven by a LIST, and there is no sensible string to promote.
130
+ text?: string
131
+ // Whether that prop is required. `project`'s folder is the one that
132
+ // is not: jostraca defaults it to `.`, and the data path must not be
133
+ // stricter than the component it drives.
134
+ req: boolean
135
+ // A prop that must be present and must be a list.
136
+ bag?: string
137
+ }
138
+
139
+ // EIGHT OF JOSTRACA'S TEN COMPONENTS. `Copy` and `List` are missing and
140
+ // cannot be added under these rules: `copy` and `list` are already
141
+ // aontu builtins with settled, unrelated meanings -- `copy(v)` copies a
142
+ // VALUE and `list()` is the list container kind, both declared in
143
+ // test/spec/signature.tsv and implemented in both ports. Lower-casing
144
+ // jostraca's names onto them would either shadow landed language
145
+ // surface or make one name mean two things by arity, and neither is a
146
+ // trade a spike gets to make. See docs/design/JOSTRACA.0.md.
147
+ const CMP_DEF: Record<string, CmpDef> = {
148
+ // The output root. Its `folder` is refused an absolute path or a
149
+ // `..` segment on the jostraca side, where the tree is data.
150
+ project: {
151
+ cmp: 'Project', text: 'folder', req: false,
152
+ children: ['project', 'folder', 'file', 'copyfiles'],
153
+ },
154
+ folder: {
155
+ cmp: 'Folder', text: 'name', req: true,
156
+ children: ['folder', 'file', 'copyfiles'],
157
+ },
158
+ file: {
159
+ cmp: 'File', text: 'name', req: true,
160
+ children: ['content', 'line', 'fragment', 'inject', 'listitems', 'copyfiles'],
161
+ },
162
+ // A span of text, exactly as written. `FileOp` joins a file's spans
163
+ // with the empty string, so a `content` carries its own terminator.
164
+ content: {
165
+ cmp: 'Content', text: 'src', req: true,
166
+ children: [],
167
+ },
168
+ // A span with a newline added, which is the whole difference.
169
+ line: {
170
+ cmp: 'Line', text: 'src', req: true,
171
+ children: [],
172
+ },
173
+ // A file read from disk with its `<[SLOT]>` markers filled.
174
+ fragment: {
175
+ cmp: 'Fragment', text: 'from', req: true,
176
+ children: ['slot', 'content', 'line', 'listitems'],
177
+ },
178
+ slot: {
179
+ cmp: 'Slot', text: 'name', req: true,
180
+ children: ['content', 'line', 'fragment', 'listitems'],
181
+ },
182
+ // A body written between markers in a file that already exists.
183
+ inject: {
184
+ cmp: 'Inject', text: 'name', req: true,
185
+ children: ['content', 'line', 'listitems'],
186
+ },
187
+ // `Copy` under a name aontu has free: `copy` is taken by the builtin
188
+ // that copies a VALUE, and a file copy is a different verb.
189
+ copyfiles: {
190
+ cmp: 'CopyFiles', text: 'from', req: true,
191
+ children: [],
192
+ },
193
+ // `List` under a name that says what it does: it renders its children
194
+ // ONCE PER ELEMENT of `item`, binding jostraca's `{item}` and
195
+ // `{item.path}` macros, and adds a trailing blank line unless
196
+ // `line: false`.
197
+ //
198
+ // IT IS THE ONE COMPONENT THE AONTU SIDE ALREADY SUBSUMES, and a
199
+ // generator should reach for `each()` first. `each($.rows, content(...))`
200
+ // repeats in the MODEL, so the repetition is finished before the tree
201
+ // exists and every produced node is data a document can reference,
202
+ // vet and diff. `listitems` defers it into jostraca's define phase behind
203
+ // a string macro aontu cannot see into -- which is the layer this
204
+ // spike exists to remove. It is here so the component set is
205
+ // complete, not because it is the better spelling.
206
+ listitems: {
207
+ cmp: 'ListItems', req: true, bag: 'item',
208
+ children: ['content', 'line', 'fragment'],
209
+ },
210
+ }
211
+
212
+ // The component name a value carries, when the value is a node this
213
+ // vocabulary built. Read structurally rather than by class, because a
214
+ // node reaches a children list as a MAP -- through a reference, a
215
+ // `each()` instance, a spread -- long after the call that made it has
216
+ // resolved away.
217
+ const BY_CMP: Record<string, string> = {}
218
+ for (const fname of Object.keys(CMP_DEF)) {
219
+ BY_CMP[CMP_DEF[fname].cmp] = fname
220
+ }
221
+
222
+ function nodeCmp(v: any): string | undefined {
223
+ if (true !== v?.isMap) {
224
+ return undefined
225
+ }
226
+ const cmp: any = v.peg?.cmp
227
+ const name = (true === cmp?.isScalar && 'string' === typeof cmp.peg) ?
228
+ cmp.peg : undefined
229
+ // The node carries the JOSTRACA name; the grammar is written in aontu
230
+ // names, so read it back through the one table.
231
+ return (undefined === name) ? undefined : BY_CMP[name]
232
+ }
233
+
234
+
235
+ // A prop's text, when the prop is a concrete string.
236
+ function propText(props: any, key: string): string | undefined {
237
+ const v: any = props?.peg?.[key]
238
+ return (true === v?.isScalar && 'string' === typeof v.peg) ? v.peg : undefined
239
+ }
240
+
241
+
242
+ class CmpFuncVal extends FuncBaseVal {
243
+ isCmpFunc = true
244
+
245
+ // The component this call builds: the function's own name, which is
246
+ // also the `cmp` key of the node and the jostraca component the
247
+ // bridge looks up.
248
+ cmp: string
249
+
250
+ constructor(
251
+ cmp: string,
252
+ spec: ValSpec,
253
+ ctx?: AontuContext
254
+ ) {
255
+ super(spec, ctx)
256
+ this.cmp = cmp
257
+ }
258
+
259
+
260
+ funcname() {
261
+ return this.cmp
262
+ }
263
+
264
+
265
+ resolve(ctx: AontuContext, args: Val[]): Val {
266
+ const def: CmpDef = CMP_DEF[this.cmp]
267
+ const leaf = 0 === def.children.length
268
+
269
+ // ARITY, refused here (see the spike-scope note above): a leaf
270
+ // takes its spec alone, a container takes a spec and an optional
271
+ // children list.
272
+ // A component whose text prop is OPTIONAL may be called with
273
+ // nothing at all: `project()` is the defaulted root, as
274
+ // `project({})` is on the other side. Everything else needs its
275
+ // spec.
276
+ if (args.length < (def.req ? 1 : 0) || args.length > (leaf ? 1 : 2)) {
277
+ return makeNilErr(ctx, 'invalid-arg', this, undefined, 'arity')
278
+ }
279
+
280
+ // THE SPEC IS A STRING OR A PROPS MAP. The string spelling fills
281
+ // the one prop the component cannot work without, so the common
282
+ // case reads as the component does -- `file("main.ts", ...)` --
283
+ // and the map spelling is there the moment a second prop is
284
+ // wanted: `file({name: "run.sh", mode: 0o755}, ...)`.
285
+ const spec: any = args[0]
286
+ let props: Val
287
+ if (undefined === spec) {
288
+ props = new MapVal({ peg: {} }, ctx)
289
+ }
290
+ else if (true === spec?.isScalar && 'string' === typeof spec.peg) {
291
+ if (undefined === def.text) {
292
+ return makeNilErr(ctx, 'invalid-arg', this, spec, 'spec')
293
+ }
294
+ props = new MapVal({ peg: { [def.text]: spec } }, ctx)
295
+ }
296
+ else if (true === spec?.isMap) {
297
+ props = spec
298
+ }
299
+ else {
300
+ return makeNilErr(ctx, 'invalid-arg', this, spec, 'spec')
301
+ }
302
+
303
+ // The one prop that is not the component's own business: without
304
+ // it there is no file to write and no content to write into one.
305
+ if (undefined !== def.text) {
306
+ const text = propText(props, def.text)
307
+ if (def.req && (undefined === text || '' === text)) {
308
+ return makeNilErr(ctx, 'invalid-arg', this, props, def.text)
309
+ }
310
+ if (!def.req && undefined !== (props as any).peg?.[def.text] &&
311
+ undefined === text) {
312
+ return makeNilErr(ctx, 'invalid-arg', this, props, def.text)
313
+ }
314
+ }
315
+
316
+ // A bag prop is required and must be a list: `listitems` with no
317
+ // `item` renders nothing, silently, which is the failure a data
318
+ // path must not have.
319
+ if (undefined !== def.bag) {
320
+ const bag: any = (props as any).peg?.[def.bag]
321
+ if (true !== bag?.isList) {
322
+ return makeNilErr(ctx, 'invalid-arg', this, props, def.bag)
323
+ }
324
+ }
325
+
326
+ // THE CHILDREN, flattened, then the containment grammar. Absent
327
+ // is empty -- an empty folder and a file with no content are both
328
+ // things a model may legitimately say -- and every node that
329
+ // survives the flattening must be one this component admits.
330
+ const kids: any = args[1]
331
+ let children: Val
332
+ if (undefined === kids) {
333
+ children = new ListVal({ peg: [] }, ctx)
334
+ }
335
+ else if (true === kids?.isList) {
336
+ const flat: Val[] = []
337
+ const splice = (list: Val[]): Val | undefined => {
338
+ for (const kid of list) {
339
+ if (true === (kid as any)?.isList) {
340
+ const bad = splice((kid as any).peg as Val[])
341
+ if (undefined !== bad) {
342
+ return bad
343
+ }
344
+ continue
345
+ }
346
+ const kcmp = nodeCmp(kid)
347
+ if (undefined === kcmp || !def.children.includes(kcmp)) {
348
+ return kid
349
+ }
350
+ flat.push(kid)
351
+ }
352
+ return undefined
353
+ }
354
+ const bad = splice(kids.peg as Val[])
355
+ if (undefined !== bad) {
356
+ return makeNilErr(ctx, 'invalid-arg', this, bad, 'children')
357
+ }
358
+ children = new ListVal({ peg: flat }, ctx)
359
+ }
360
+ else {
361
+ return makeNilErr(ctx, 'invalid-arg', this, kids, 'children')
362
+ }
363
+
364
+ const node = new MapVal({
365
+ peg: {
366
+ cmp: new StringVal({ peg: def.cmp }, ctx),
367
+ props,
368
+ children,
369
+ }
370
+ }, ctx)
371
+
372
+ // Closed: the three keys ARE the node vocabulary (see the note
373
+ // above). `props` is left open on purpose.
374
+ node.closed = true
375
+
376
+ return this.place(node)
377
+ }
378
+
379
+ } /* node:coverage ignore next 3 */
380
+
381
+
382
+ // ONE CLASS PER COMPONENT, generated from the table, because the
383
+ // registry constructs by `new funcval({peg: args})` and so needs a
384
+ // constructor per name. Written as a factory rather than eight
385
+ // near-identical subclasses: the table is the single statement of what
386
+ // exists, and a ninth component is a row in it and nothing else.
387
+ function cmpFuncClass(fname: string): any {
388
+ class Cmp extends CmpFuncVal {
389
+ constructor(spec: ValSpec, ctx?: AontuContext) {
390
+ super(fname, spec, ctx)
391
+ }
392
+
393
+ make(_ctx: AontuContext, spec: ValSpec): Val {
394
+ return new Cmp(spec)
395
+ }
396
+ }
397
+ return Cmp
398
+ }
399
+
400
+
401
+ const CMP_FUNCS: Record<string, any> = {}
402
+ for (const fname of Object.keys(CMP_DEF)) {
403
+ CMP_FUNCS[fname] = cmpFuncClass(fname)
404
+ } /* node:coverage ignore next 7 */
405
+
406
+
407
+ export {
408
+ CMP_DEF,
409
+ CMP_FUNCS,
410
+ CmpFuncVal,
411
+ }
@@ -1,26 +1,42 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
2
 
3
- // GENERATION: `each(data, tmpl)` (G8 phase 1,
4
- // docs/capability-review/g8-generation.md). One element of the result
5
- // list per child of `data`, each of them that child met with `tmpl`.
6
- // Written with one argument, `each(m)` is a map's children as a list.
3
+ // GENERATION TO A LIST: `each(data, tmpl)` (G9 §4 as `form`;
4
+ // docs/design/RENDER.0.md D11 and P6; renamed by ADR-027). One element
5
+ // of the result list per child of `data`, being `tmpl` instantiated at
6
+ // that position with `_` bound to the source child. It REPLACES; it
7
+ // does not meet.
7
8
  //
8
- // ports: {http: 80, https: 443}
9
- // open: each($.ports, integer) -> [80, 443]
9
+ // names: [web, auth]
10
+ // units: each($.names, {path: _ + ".ts"}) -> [{path: "web.ts"}, {path: "auth.ts"}]
10
11
  //
11
- // ORDER IS FIXED, and fixed the way canon already fixes it: source
12
- // order for a list, sorted-key order for a map (`cmpCodePoint`, the
13
- // comparator `MapVal.canon` and the Go port's marshalling use). A
14
- // generated list whose order depended on insertion history would differ
15
- // between two runs of the same document, and between the two ports --
16
- // the one thing the shared spec exists to prevent.
12
+ // AND `_ & t` IS THE BOUND, because a meet is a template away. Putting
13
+ // the source child back into what is built makes the element that
14
+ // child MET with `t` rather than `t` alone:
17
15
  //
18
- // The element is the source child CLONED, not shared: it is a second
19
- // position holding that value, and a position is where path-dependent
20
- // content resolves. What the clone keeps is the identity, if the child
21
- // carries one (G4 phase 1) -- a listed entity is still that entity, and
22
- // the merge that follows is the point.
23
-
16
+ // each($.ports, _ & integer) every element is an integer
17
+ // each($.m, _) a map's members as a list
18
+ //
19
+ // One generator, two idioms, and the `_` says which: mention the hole
20
+ // and the child survives, leave it out and the template stands alone.
21
+ // That is why the old meet-only `each` was retired (ADR-026) before
22
+ // this function took its name (ADR-027) -- one spelling per concept,
23
+ // and `_` was already the spelling for "the child here".
24
+ //
25
+ // WHY IT EXISTS AT ALL: ORDER. `pick(pack(d, {f: t}), f)` maps too,
26
+ // but it goes through a map and re-sorts to code-point order, and it
27
+ // refuses a list of records outright (pack_key). A struct's fields, a
28
+ // DDL's columns and a file's imports are lists whose order is the
29
+ // model's, and silently alphabetising them is wrong output. `each`
30
+ // reads its members through the one ordering helper every bag reader
31
+ // uses (members.ts), so no two can disagree about order -- source
32
+ // order for a list, sorted-key order for a map -- and it skips what
33
+ // generation would not emit: a hidden child, an unfilled optional.
34
+ //
35
+ // It joins `boundArgStart` (PlaceVal.ts): a `_` inside its template
36
+ // is ITS hole to bind, never an outer generator's. Forgetting that is
37
+ // BUGS.md §34's silent failure -- every existing test passes and the
38
+ // new combinator captures an outer generator's hole -- which is why
39
+ // the shared spec's nesting rows exist.
24
40
  import type {
25
41
  Val,
26
42
  ValSpec,
@@ -30,7 +46,6 @@ import {
30
46
  AontuContext,
31
47
  } from '../ctx'
32
48
 
33
- import { unite } from '../unify'
34
49
  import { makeNilErr } from '../err'
35
50
  import { ListVal } from './ListVal'
36
51
  import { FuncBaseVal } from './FuncBaseVal'
@@ -39,19 +54,11 @@ import { fillPlace } from './PlaceVal'
39
54
  import { memberVals } from './members'
40
55
 
41
56
 
42
- // The members a data bag holds, in the order the result must carry
43
- // them -- what generation would emit (./members.ts, BUGS.md §79) --
44
- // or the code naming what is wrong with the argument.
45
- function dataValues(data: Val | undefined, ctx: AontuContext): Val[] | string {
46
- const vals = memberVals(data, ctx)
47
- return undefined === vals ? 'each_data' : vals
48
- }
49
-
50
-
51
57
  class EachFuncVal extends FuncBaseVal {
52
58
  isEachFunc = true
53
59
 
54
- // THE STAGING RULE, for the reason given in PackFuncVal.
60
+ // THE STAGING RULE, for the reason given in PackFuncVal: the data
61
+ // is not settled merely by being `done` once.
55
62
  staged = true
56
63
 
57
64
  constructor(
@@ -62,7 +69,6 @@ class EachFuncVal extends FuncBaseVal {
62
69
  }
63
70
 
64
71
 
65
-
66
72
  funcname() {
67
73
  return 'each'
68
74
  }
@@ -76,8 +82,7 @@ class EachFuncVal extends FuncBaseVal {
76
82
 
77
83
  unify(peer: Val, ctx: AontuContext): Val {
78
84
  // ONE argument is driven: the data. The template is not (see
79
- // prepare above), and driveStagedArgs answers whether the data has
80
- // settled -- the other half of "ready to fire".
85
+ // prepare above).
81
86
  if (!this.stagedReady(peer, ctx, 1)) {
82
87
  return this.residuate(peer, ctx)
83
88
  }
@@ -87,37 +92,31 @@ class EachFuncVal extends FuncBaseVal {
87
92
 
88
93
 
89
94
  resolve(ctx: AontuContext, args: Val[]) {
90
- const vals = dataValues(args?.[0], ctx)
91
- if ('string' === typeof vals) {
92
- return makeNilErr(ctx, vals, this)
95
+ const vals = memberVals(args?.[0], ctx)
96
+ if (undefined === vals) {
97
+ return makeNilErr(ctx, 'each_data', this)
93
98
  }
94
99
 
95
- const tmpl: Val | undefined = args?.[1]
100
+ // Arity is checked at parse (funcArity), so the template is here.
101
+ const tmpl: Val = args[1]
96
102
  const peg: Val[] = []
97
103
 
98
104
  for (let i = 0; i < vals.length; i++) {
99
105
  const elctx = ctx.descend(String(i))
100
- const el = vals[i].clone(elctx)
101
- // The template is CLONED per element, never shared — a FULL
102
- // instance to the leaves (`dup`, ADR-005): see PackFuncVal.resolve.
103
- // `_` inside the template binds the source child (G8 phase 3),
104
- // which for `each` is the element itself.
105
- let inst: Val | undefined = undefined
106
- if (undefined !== tmpl) {
107
- inst = tmpl.clone(elctx, { dup: true })
108
- repathInstance(inst, inst.path)
109
- }
110
- peg.push(undefined === inst ? el :
111
- unite(elctx, el, fillPlace(inst, vals[i], elctx), 'each'))
106
+ // A FULL INSTANCE per element, to the leaves (`dup`, ADR-005),
107
+ // at the element's own position -- see PackFuncVal.resolve --
108
+ // with `_` bound to the source child and NOTHING met into it.
109
+ const inst = tmpl.clone(elctx, { dup: true })
110
+ repathInstance(inst, inst.path)
111
+ peg.push(fillPlace(inst, vals[i], elctx))
112
112
  }
113
113
 
114
114
  return new ListVal({ peg }, ctx)
115
115
  }
116
116
 
117
- } /* node:coverage ignore next 7 */
117
+ } /* node:coverage ignore next 6 */
118
118
 
119
119
 
120
120
  export {
121
- dataValues,
122
121
  EachFuncVal,
123
122
  }
@@ -16,6 +16,7 @@ import { makeNilErr } from '../err'
16
16
  import { ScalarKindVal } from '../val/ScalarKindVal'
17
17
  import { makeScalarLike } from '../val/valutil'
18
18
  import { Decimal } from '../val/Decimal'
19
+ import { caseRange, rangeArg } from './caserange'
19
20
 
20
21
 
21
22
 
@@ -49,7 +50,23 @@ class LowerFuncVal extends FuncBaseVal {
49
50
  // internal error.
50
51
  const arg = args?.[0]
51
52
  const oldpeg = arg?.peg
52
- const peg = 'string' === typeof oldpeg ? oldpeg.toLowerCase() :
53
+
54
+ // THE RANGE (ts/src/val/caserange.ts): `start` names the first
55
+ // character of the run when it is zero or positive and the last
56
+ // when it is negative; `len` of -1, and the absent argument, are
57
+ // the source's length. Refused on a NUMBER, where a run of
58
+ // characters means nothing -- the numeric arm below is a ceiling,
59
+ // not a case mapping.
60
+ const start = rangeArg(args?.[1])
61
+ const len = rangeArg(args?.[2])
62
+ const ranged = undefined !== start || undefined !== len
63
+ if (ranged &&
64
+ (Number.isNaN(start as number) || Number.isNaN(len as number) ||
65
+ 'string' !== typeof oldpeg)) {
66
+ return this.place(makeNilErr(ctx, 'invalid-arg', this, arg, 'range'))
67
+ }
68
+ const peg = 'string' === typeof oldpeg ?
69
+ caseRange(oldpeg, start ?? 0, len ?? -1, false) :
53
70
  'number' === typeof oldpeg ? Math.floor(oldpeg) :
54
71
  // The exact leaves take an EXACT floor and keep their kind: a
55
72
  // biginteger is already integral so it is its own floor, and a
package/src/val/NilVal.ts CHANGED
@@ -98,9 +98,35 @@ class NilVal extends Val {
98
98
  // unconditionally was tried and reverted -- it moved the closed-key,
99
99
  // spread-template and every `--at` finding to the driving location,
100
100
  // which is not where those belong.
101
- if (null != ctx?.path && null != nil.path &&
102
- nil.path.length < ctx.path.length &&
103
- nil.path.every((p, i) => p === ctx.path[i])) {
101
+ // A MEET OF TWO OPERANDS HAPPENS AT THE SLOT, always. A value that
102
+ // arrives by REFERENCE carries a path re-based onto the referring
103
+ // field -- `$.p.Port.direction` for a `p: $.lib.Port` whose
104
+ // `direction` conflicts -- and the two ports corrupt it DIFFERENTLY
105
+ // (Go's fallback is the slot plus the schema's own tail), so neither
106
+ // operand path is trustworthy once a reference is in play. The
107
+ // prefix test below cannot tell the corrupt case from a legitimately
108
+ // deeper one, because Go's corrupt path EXTENDS the right answer.
109
+ // The slot is the one thing both ports know exactly, so a real meet
110
+ // takes it.
111
+ //
112
+ // A SINGLE-OPERAND nil is left alone: a residue, a closed key, a
113
+ // generation failure and an `--at` finding are not meets, they are
114
+ // facts about one value, and taking the driving location for those
115
+ // is what the unconditional version got wrong. The ONE exception is
116
+ // an operand that was MINTED -- a preference's yardstick, an
117
+ // arithmetic or concat result -- which carries no path at all, so a
118
+ // conflict at `$.a` reported `$`, the whole document rather than the
119
+ // key to edit.
120
+ //
121
+ // This used to ask whether the operand's path was a PREFIX of the
122
+ // slot, and extend it when so. Once a meet takes the slot outright
123
+ // the question is vacuous: every nil still reaching here carries
124
+ // either no path or one at least as long as the slot, so the prefix
125
+ // walk never ran a single comparison across the whole suite. ADR-002
126
+ // closes unreachable code by deleting it rather than by covering it,
127
+ // and the emptiness test below is what the walk actually decided.
128
+ if (null != ctx?.path && 0 < ctx.path.length &&
129
+ (null != bv || null == nil.path || 0 === nil.path.length)) {
104
130
  nil.path = [...ctx.path]
105
131
  }
106
132