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.
- package/README.md +3 -3
- package/dist/agentsmd.d.ts +1 -0
- package/dist/agentsmd.js +7 -1
- package/dist/agentsmd.js.map +1 -1
- package/dist/allow.d.ts +23 -0
- package/dist/allow.js +230 -0
- package/dist/allow.js.map +1 -0
- package/dist/aontu.d.ts +4 -2
- package/dist/aontu.js +4 -2
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +1000 -47
- package/dist/cli.js.map +1 -1
- package/dist/format.d.ts +1 -0
- package/dist/format.js +154 -12
- package/dist/format.js.map +1 -1
- package/dist/helpdoc.d.ts +16 -0
- package/dist/helpdoc.js +59 -0
- package/dist/helpdoc.js.map +1 -0
- package/dist/hints.js +15 -2
- package/dist/hints.js.map +1 -1
- package/dist/lang.js +93 -40
- package/dist/lang.js.map +1 -1
- package/dist/lower.d.ts +3 -0
- package/dist/lower.js +3 -0
- package/dist/lower.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.js +1 -1
- package/dist/lsp.js.map +1 -1
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +7 -1
- package/dist/relation.js.map +1 -1
- package/dist/render.js +26 -21
- package/dist/render.js.map +1 -1
- package/dist/sigdecl.js +1 -1
- package/dist/sigdecl.js.map +1 -1
- package/dist/std.js +112 -69
- package/dist/std.js.map +1 -1
- package/dist/template.d.ts +2 -1
- package/dist/template.js +51 -15
- package/dist/template.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/val/AggFuncVal.js +2 -2
- package/dist/val/AggFuncVal.js.map +1 -1
- package/dist/val/CmpFuncVal.d.ts +20 -0
- package/dist/val/CmpFuncVal.js +245 -0
- package/dist/val/CmpFuncVal.js.map +1 -0
- package/dist/val/EachFuncVal.d.ts +1 -2
- package/dist/val/EachFuncVal.js +15 -29
- package/dist/val/EachFuncVal.js.map +1 -1
- package/dist/val/FormFuncVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js +17 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/NamerFuncVal.d.ts +12 -0
- package/dist/val/NamerFuncVal.js +176 -0
- package/dist/val/NamerFuncVal.js.map +1 -0
- package/dist/val/NilVal.js +29 -3
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/NomFuncVal.d.ts +12 -0
- package/dist/val/NomFuncVal.js +187 -0
- package/dist/val/NomFuncVal.js.map +1 -0
- package/dist/val/PackFuncVal.js +3 -3
- package/dist/val/PackFuncVal.js.map +1 -1
- package/dist/val/RefVal.js +1 -1
- package/dist/val/TranslateFuncVal.d.ts +12 -0
- package/dist/val/TranslateFuncVal.js +101 -0
- package/dist/val/TranslateFuncVal.js.map +1 -0
- package/dist/val/UpperFuncVal.js +17 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/caserange.d.ts +3 -0
- package/dist/val/caserange.js +110 -0
- package/dist/val/caserange.js.map +1 -0
- package/dist/vet.d.ts +12 -0
- package/dist/vet.js +209 -1
- package/dist/vet.js.map +1 -1
- package/grammar/aontu.abnf +1 -1
- package/grammar/aontu.gbnf +1 -1
- package/grammar/aontu.lark +1 -1
- package/grammar/aontu.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/skill/init/check.sh +28 -0
- package/skill/init/data.aon +12 -0
- package/skill/init/model.aon +19 -0
- package/skill/tasks.md +151 -0
- package/src/agentsmd.ts +13 -2
- package/src/allow.ts +316 -0
- package/src/aontu.ts +10 -1
- package/src/cli.ts +1131 -53
- package/src/format.ts +191 -14
- package/src/helpdoc.ts +77 -0
- package/src/hints.ts +16 -2
- package/src/lang.ts +98 -42
- package/src/lower.ts +3 -3
- package/src/lsp.ts +1 -1
- package/src/relation.ts +21 -1
- package/src/render.ts +26 -21
- package/src/sigdecl.ts +1 -1
- package/src/std.ts +112 -69
- package/src/template.ts +55 -15
- package/src/val/AggFuncVal.ts +2 -2
- package/src/val/CmpFuncVal.ts +411 -0
- package/src/val/EachFuncVal.ts +49 -50
- package/src/val/LowerFuncVal.ts +18 -1
- package/src/val/NilVal.ts +29 -3
- package/src/val/NomFuncVal.ts +287 -0
- package/src/val/PackFuncVal.ts +3 -3
- package/src/val/RefVal.ts +1 -1
- package/src/val/TranslateFuncVal.ts +182 -0
- package/src/val/UpperFuncVal.ts +18 -1
- package/src/val/caserange.ts +115 -0
- package/src/vet.ts +287 -1
- 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
|
+
}
|
package/src/val/EachFuncVal.ts
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
1
|
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
2
|
|
|
3
|
-
// GENERATION: `each(data, tmpl)` (
|
|
4
|
-
// docs/
|
|
5
|
-
// list per child of `data`,
|
|
6
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// names: [web, auth]
|
|
10
|
+
// units: each($.names, {path: _ + ".ts"}) -> [{path: "web.ts"}, {path: "auth.ts"}]
|
|
10
11
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
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)
|
|
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 =
|
|
91
|
-
if (
|
|
92
|
-
return makeNilErr(ctx,
|
|
95
|
+
const vals = memberVals(args?.[0], ctx)
|
|
96
|
+
if (undefined === vals) {
|
|
97
|
+
return makeNilErr(ctx, 'each_data', this)
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
117
|
+
} /* node:coverage ignore next 6 */
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
export {
|
|
121
|
-
dataValues,
|
|
122
121
|
EachFuncVal,
|
|
123
122
|
}
|
package/src/val/LowerFuncVal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|