@hyperscale0/hsx 1.0.332 → 1.0.335
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/attachment-economics.d.ts +16 -2
- package/dist/src/attachment-economics.d.ts.map +1 -1
- package/dist/src/attachment-economics.js +58 -2
- package/dist/src/attachment-economics.js.map +1 -1
- package/dist/src/catalog-program.d.ts +7 -0
- package/dist/src/catalog-program.d.ts.map +1 -0
- package/dist/src/catalog-program.js +66 -0
- package/dist/src/catalog-program.js.map +1 -0
- package/dist/src/cli.js +2 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +5 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +47 -5
- package/dist/src/compile.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/parse.js +13 -0
- package/dist/src/parse.js.map +1 -1
- package/dist/src/template-guide.d.ts +57 -0
- package/dist/src/template-guide.d.ts.map +1 -0
- package/dist/src/template-guide.js +435 -0
- package/dist/src/template-guide.js.map +1 -0
- package/docs/README.md +11 -1
- package/package.json +4 -4
- package/src/attachment-economics.ts +77 -2
- package/src/catalog-program.ts +65 -0
- package/src/cli.ts +2 -2
- package/src/compile.ts +73 -8
- package/src/index.ts +7 -0
- package/src/parse.ts +15 -0
- package/src/template-guide.ts +584 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import type { BlockExpr, Entry, Expr, InstrumentDecl } from "./ast.ts";
|
|
2
|
+
import { catalogProgramSource } from "./catalog-program.ts";
|
|
3
|
+
import { compile, type CompileOptions } from "./compile.ts";
|
|
4
|
+
import { parseHeader } from "./header-source.ts";
|
|
5
|
+
import { headerManifest, HEADER_NAMES } from "./headers.ts";
|
|
6
|
+
import { parseProgram } from "./parse.ts";
|
|
7
|
+
import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
|
|
8
|
+
|
|
9
|
+
export interface TemplateIndexEntry {
|
|
10
|
+
qualifiedName: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
parties: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TemplateGuideAction {
|
|
16
|
+
name: string;
|
|
17
|
+
runBy: string;
|
|
18
|
+
from?: string;
|
|
19
|
+
to?: string;
|
|
20
|
+
due?: string;
|
|
21
|
+
deadline?: string;
|
|
22
|
+
requires?: string[];
|
|
23
|
+
moves?: string[];
|
|
24
|
+
invokes?: string[];
|
|
25
|
+
when?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TemplateGuide {
|
|
29
|
+
qualifiedName: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
signature: string;
|
|
32
|
+
use: string;
|
|
33
|
+
states: string[];
|
|
34
|
+
tunables: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
required: boolean;
|
|
38
|
+
default?: string;
|
|
39
|
+
meaning: string;
|
|
40
|
+
}>;
|
|
41
|
+
createInput: Array<{ name: string; type: string }>;
|
|
42
|
+
subject: Array<{ name: string; type: string }>;
|
|
43
|
+
actions: TemplateGuideAction[];
|
|
44
|
+
example: string;
|
|
45
|
+
notes: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// How to read runBy, the same for every template.
|
|
49
|
+
const RUN_BY_NOTE =
|
|
50
|
+
"runBy names who runs each action: a party is whoever the attach binds to it (owner, actor, operator or a declared business), clock is the platform when the action falls due, parent runs only inside its parent record's action, and caller is anyone the exposed action admits.";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A stock money move declares no purpose, because the purpose follows how
|
|
54
|
+
* the attach binds its parties; the guide shows the line for this example.
|
|
55
|
+
*/
|
|
56
|
+
function economicsNote(lines: string[]) {
|
|
57
|
+
return `economics declares what a payment is for the books. Add ${lines.map((line) => `\`${line}\``).join(" and ")} inside the attach. Use purpose earning when operator receives the money as revenue, participant_payout when operator pays a participant, and pass_through when money passes between participants. Without it hsx check warns and the books report leaves the payment out.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SUBJECT_NOTE =
|
|
61
|
+
"subject names fields the attached object must carry, or rename to with rename { field: yourField }, before the actions that read them can run.";
|
|
62
|
+
|
|
63
|
+
/** One line per template, small enough to read whole. */
|
|
64
|
+
export function templateIndex(
|
|
65
|
+
library: StandardLibrary = bundledStandardLibrary,
|
|
66
|
+
): TemplateIndexEntry[] {
|
|
67
|
+
return headerManifest(library).headers.flatMap((header) =>
|
|
68
|
+
header.objects.map((object) => ({
|
|
69
|
+
qualifiedName: object.qualifiedName,
|
|
70
|
+
summary: object.summary,
|
|
71
|
+
parties: object.parties,
|
|
72
|
+
})),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Everything an author needs for one template, spelled from its std
|
|
78
|
+
* declaration and proved by compiling its example. Undefined when the
|
|
79
|
+
* catalog has no such template.
|
|
80
|
+
*/
|
|
81
|
+
export function templateGuide(
|
|
82
|
+
qualifiedName: string,
|
|
83
|
+
options: GuideOptions = {},
|
|
84
|
+
): TemplateGuide | undefined {
|
|
85
|
+
const library = options.library ?? bundledStandardLibrary;
|
|
86
|
+
const [header, name, extra] = qualifiedName.split(".");
|
|
87
|
+
if (!header || !name || extra !== undefined) return undefined;
|
|
88
|
+
if (!(HEADER_NAMES as readonly string[]).includes(header)) return undefined;
|
|
89
|
+
const source = library.source(header);
|
|
90
|
+
if (!source) return undefined;
|
|
91
|
+
const manifest = headerManifest(library, [header]).headers[0]!.objects.find(
|
|
92
|
+
(object) => object.name === name,
|
|
93
|
+
);
|
|
94
|
+
const decl = parseHeader(source, header).decls.find(
|
|
95
|
+
(d): d is InstrumentDecl => d.kind === "instrument" && d.name === name,
|
|
96
|
+
);
|
|
97
|
+
if (!manifest || !decl) return undefined;
|
|
98
|
+
const spell = (expression: Expr) =>
|
|
99
|
+
source
|
|
100
|
+
.slice(expression.span.start, expression.span.end)
|
|
101
|
+
.trim()
|
|
102
|
+
.replace(/\s+/g, " ");
|
|
103
|
+
const actions = guideActions(decl.body, spell);
|
|
104
|
+
// Compile once bare, then expose each root action a party or caller runs
|
|
105
|
+
// that these bindings keep, and compile again to prove the example.
|
|
106
|
+
const example = exampleProgram(qualifiedName, library);
|
|
107
|
+
const bare = compiledAttachment(
|
|
108
|
+
example.render([]),
|
|
109
|
+
example.attachment,
|
|
110
|
+
options,
|
|
111
|
+
);
|
|
112
|
+
const exposes = actions
|
|
113
|
+
.filter(
|
|
114
|
+
(action) =>
|
|
115
|
+
!action.name.includes(".") &&
|
|
116
|
+
!["clock", "parent"].includes(action.runBy) &&
|
|
117
|
+
bare.actions[action.name],
|
|
118
|
+
)
|
|
119
|
+
.map((action) => `expose ${action.name}`);
|
|
120
|
+
const program = example.render(exposes);
|
|
121
|
+
const compiled = compiledAttachment(program, example.attachment, options);
|
|
122
|
+
const create = compiled.actions.create;
|
|
123
|
+
const economics = compiled.warnings.flatMap((warning) =>
|
|
124
|
+
warning.message.startsWith(`${example.attachment}: `)
|
|
125
|
+
? [/economics [^}]+\}/.exec(warning.fix)![0]]
|
|
126
|
+
: [],
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
qualifiedName,
|
|
130
|
+
summary: manifest.summary,
|
|
131
|
+
signature: manifest.signature,
|
|
132
|
+
use: `use ${header}`,
|
|
133
|
+
states: manifest.states,
|
|
134
|
+
tunables: manifest.tunables.map((tunable) => ({
|
|
135
|
+
name: tunable.name,
|
|
136
|
+
type: tunable.type,
|
|
137
|
+
required: tunable.required,
|
|
138
|
+
...(tunable.default ? { default: tunable.default } : {}),
|
|
139
|
+
meaning:
|
|
140
|
+
tunable.type === "party" || tunable.type === "party?"
|
|
141
|
+
? partyMeaning(tunable.name, actions)
|
|
142
|
+
: valueMeaning(tunable.name, tunable.default, decl, spell),
|
|
143
|
+
})),
|
|
144
|
+
createInput: (create?.input ?? []).map((field) => ({
|
|
145
|
+
name: field.name,
|
|
146
|
+
type: inputType(field as unknown as Record<string, unknown>),
|
|
147
|
+
})),
|
|
148
|
+
subject: manifest.subject,
|
|
149
|
+
actions,
|
|
150
|
+
example: program,
|
|
151
|
+
notes: [
|
|
152
|
+
RUN_BY_NOTE,
|
|
153
|
+
...(manifest.subject.length ? [SUBJECT_NOTE] : []),
|
|
154
|
+
...(economics.length ? [economicsNote(economics)] : []),
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function slot(block: BlockExpr, key: string) {
|
|
160
|
+
return block.entries.find((entry) => entry.key === key)?.value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The parser desugars `requires` and `moves` into blocks without source
|
|
165
|
+
* spans, so each clause is spelled from the slot's own source text.
|
|
166
|
+
*/
|
|
167
|
+
function clauses(
|
|
168
|
+
action: BlockExpr,
|
|
169
|
+
key: "requires" | "moves",
|
|
170
|
+
spell: (expression: Expr) => string,
|
|
171
|
+
) {
|
|
172
|
+
return action.entries
|
|
173
|
+
.filter((entry) => entry.key === key)
|
|
174
|
+
.flatMap((entry) =>
|
|
175
|
+
spell(entry.value)
|
|
176
|
+
.split(new RegExp(`(?:^|[;,]?\\s+)${key}\\s+`))
|
|
177
|
+
.map((clause) => clause.trim().replace(/[;,]$/, ""))
|
|
178
|
+
.filter(Boolean),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function items(expression: Expr | undefined): Expr[] {
|
|
183
|
+
if (!expression) return [];
|
|
184
|
+
return expression.kind === "list" ? expression.items : [expression];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Root actions first, then each record's, prefixed with the record name. */
|
|
188
|
+
function guideActions(
|
|
189
|
+
body: BlockExpr,
|
|
190
|
+
spell: (expression: Expr) => string,
|
|
191
|
+
prefix = "",
|
|
192
|
+
parentActions: TemplateGuideAction[] = [],
|
|
193
|
+
): TemplateGuideAction[] {
|
|
194
|
+
const result: TemplateGuideAction[] = [];
|
|
195
|
+
for (const entry of body.entries) {
|
|
196
|
+
if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
|
|
197
|
+
continue;
|
|
198
|
+
const action = entry.value;
|
|
199
|
+
const actor = slot(action, "actor");
|
|
200
|
+
const party = actor?.kind === "block" ? slot(actor, "party") : undefined;
|
|
201
|
+
const parent = actor?.kind === "block" ? slot(actor, "parent") : undefined;
|
|
202
|
+
const runBy = !actor
|
|
203
|
+
? "caller"
|
|
204
|
+
: party
|
|
205
|
+
? spell(party)
|
|
206
|
+
: parent
|
|
207
|
+
? "parent"
|
|
208
|
+
: spell(actor);
|
|
209
|
+
const at = (key: string) => {
|
|
210
|
+
const value = slot(action, key);
|
|
211
|
+
if (value?.kind !== "block") return value ? spell(value) : undefined;
|
|
212
|
+
const instant = slot(value, "at");
|
|
213
|
+
const offset = slot(value, "offset");
|
|
214
|
+
if (!instant) return spell(value);
|
|
215
|
+
return offset ? `${spell(instant)} + ${spell(offset)}` : spell(instant);
|
|
216
|
+
};
|
|
217
|
+
const due = at("due");
|
|
218
|
+
const deadline = at("deadline");
|
|
219
|
+
const requires = clauses(action, "requires", spell);
|
|
220
|
+
const moves = clauses(action, "moves", spell);
|
|
221
|
+
const invokes = items(slot(action, "invoke")).flatMap((item) =>
|
|
222
|
+
item.kind === "block" ? [invocation(item, spell, prefix)] : [],
|
|
223
|
+
);
|
|
224
|
+
const from = slot(action, "from");
|
|
225
|
+
const to = slot(action, "to");
|
|
226
|
+
const guide: TemplateGuideAction = {
|
|
227
|
+
name: prefix + entry.key.slice(7),
|
|
228
|
+
runBy,
|
|
229
|
+
...(from ? { from: spell(from) } : {}),
|
|
230
|
+
...(to ? { to: spell(to) } : {}),
|
|
231
|
+
...(due ? { due } : {}),
|
|
232
|
+
...(deadline ? { deadline } : {}),
|
|
233
|
+
...(requires.length ? { requires } : {}),
|
|
234
|
+
...(moves.length ? { moves } : {}),
|
|
235
|
+
...(invokes.length ? { invokes } : {}),
|
|
236
|
+
};
|
|
237
|
+
const when = timing(guide, parentActions);
|
|
238
|
+
result.push(when ? { ...guide, when } : guide);
|
|
239
|
+
}
|
|
240
|
+
const records = slot(body, "records");
|
|
241
|
+
if (records?.kind === "block")
|
|
242
|
+
for (const record of records.entries)
|
|
243
|
+
if (record.value.kind === "block")
|
|
244
|
+
result.push(
|
|
245
|
+
...guideActions(
|
|
246
|
+
record.value,
|
|
247
|
+
spell,
|
|
248
|
+
`${prefix}${record.key}.`,
|
|
249
|
+
prefix ? parentActions : result,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function invocation(
|
|
256
|
+
block: BlockExpr,
|
|
257
|
+
spell: (expression: Expr) => string,
|
|
258
|
+
prefix: string,
|
|
259
|
+
) {
|
|
260
|
+
const instrument = slot(block, "instrument");
|
|
261
|
+
const action = slot(block, "action");
|
|
262
|
+
const target = `${instrument ? `${prefix}${spell(instrument)}.` : ""}${action ? spell(action) : "?"}`;
|
|
263
|
+
const range = slot(block, "range");
|
|
264
|
+
const count = range?.kind === "block" ? slot(range, "count") : undefined;
|
|
265
|
+
const literal =
|
|
266
|
+
count?.kind === "block" ? (slot(count, "literal") ?? count) : count;
|
|
267
|
+
const selection = slot(block, "selection");
|
|
268
|
+
const selected =
|
|
269
|
+
selection?.kind === "block" ? slot(selection, "instrument") : undefined;
|
|
270
|
+
return literal
|
|
271
|
+
? `${target}, ${spell(literal)} times`
|
|
272
|
+
: selected
|
|
273
|
+
? `${target} on each matching ${spell(selected)}`
|
|
274
|
+
: target;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The runtime's timing rules, applied to one action: a clock action that
|
|
279
|
+
* needs a record in another state waits, and the action that moves the
|
|
280
|
+
* record there collects it in the same request (docs/public/runtime.md).
|
|
281
|
+
*/
|
|
282
|
+
function timing(
|
|
283
|
+
action: TemplateGuideAction,
|
|
284
|
+
parentActions: TemplateGuideAction[],
|
|
285
|
+
): string | undefined {
|
|
286
|
+
if (action.runBy === "clock" && action.due) {
|
|
287
|
+
const waits = (action.requires ?? []).flatMap((requirement) => {
|
|
288
|
+
const match = requirement.match(/^self\.\w+ in \[([^\]]+)\]$/);
|
|
289
|
+
return match ? [{ requirement, states: match[1]!.split(/,\s*/) }] : [];
|
|
290
|
+
});
|
|
291
|
+
const openers = parentActions
|
|
292
|
+
.filter((parent) =>
|
|
293
|
+
waits.some((wait) => wait.states.includes(parent.to ?? "")),
|
|
294
|
+
)
|
|
295
|
+
.map((parent) => parent.name);
|
|
296
|
+
return [
|
|
297
|
+
`The clock runs it at ${action.due}.`,
|
|
298
|
+
...waits.map(
|
|
299
|
+
(wait) => `While ${wait.requirement} does not hold, it waits.`,
|
|
300
|
+
),
|
|
301
|
+
...(openers.length
|
|
302
|
+
? [
|
|
303
|
+
`${openers.join(" or ")} collects every one already due in the same request.`,
|
|
304
|
+
]
|
|
305
|
+
: []),
|
|
306
|
+
].join(" ");
|
|
307
|
+
}
|
|
308
|
+
const parts = [
|
|
309
|
+
...(action.due
|
|
310
|
+
? [`Runnable from ${action.due}; ${action.runBy} still runs it.`]
|
|
311
|
+
: []),
|
|
312
|
+
...(action.deadline ? [`Refused after ${action.deadline}.`] : []),
|
|
313
|
+
];
|
|
314
|
+
return parts.length ? parts.join(" ") : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function partyMeaning(name: string, actions: TemplateGuideAction[]) {
|
|
318
|
+
const runs = actions.filter((action) => action.runBy === name);
|
|
319
|
+
const pays = actions.filter((action) =>
|
|
320
|
+
action.moves?.some((move) => new RegExp(`\\bfrom ${name}\\b`).test(move)),
|
|
321
|
+
);
|
|
322
|
+
const receives = actions.filter((action) =>
|
|
323
|
+
action.moves?.some((move) => new RegExp(`\\bto ${name}\\b`).test(move)),
|
|
324
|
+
);
|
|
325
|
+
const names = (list: TemplateGuideAction[]) =>
|
|
326
|
+
list.map((action) => action.name).join(", ");
|
|
327
|
+
const parts = [
|
|
328
|
+
...(runs.length ? [`Runs ${names(runs)}.`] : []),
|
|
329
|
+
...(pays.length ? [`Pays in ${names(pays)}.`] : []),
|
|
330
|
+
...(receives.length ? [`Receives in ${names(receives)}.`] : []),
|
|
331
|
+
];
|
|
332
|
+
return parts.length
|
|
333
|
+
? parts.join(" ")
|
|
334
|
+
: "Named by the template; runs no action and moves no money itself.";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A value tunable's meaning is where it lands: the fields it sets and every
|
|
339
|
+
* field or action that reads those fields, spelled as the std writes them.
|
|
340
|
+
*/
|
|
341
|
+
function valueMeaning(
|
|
342
|
+
name: string,
|
|
343
|
+
fallback: string | undefined,
|
|
344
|
+
decl: InstrumentDecl,
|
|
345
|
+
spell: (expression: Expr) => string,
|
|
346
|
+
) {
|
|
347
|
+
const bare = new RegExp(`(?<![\\w.])${name}(?!\\w)`);
|
|
348
|
+
const fields = slot(decl.body, "fields");
|
|
349
|
+
const set =
|
|
350
|
+
fields?.kind === "block"
|
|
351
|
+
? fields.entries.filter((entry) => bare.test(fieldValue(entry, spell)))
|
|
352
|
+
: [];
|
|
353
|
+
const read = new Set(set.map((entry) => entry.key));
|
|
354
|
+
const uses: string[] = [];
|
|
355
|
+
const visit = (body: BlockExpr, prefix: string, parentRefs: string[]) => {
|
|
356
|
+
const own = slot(body, "fields");
|
|
357
|
+
const reads = (text: string) =>
|
|
358
|
+
bare.test(text) ||
|
|
359
|
+
[...read].some((field) =>
|
|
360
|
+
prefix
|
|
361
|
+
? parentRefs.some((ref) =>
|
|
362
|
+
new RegExp(`\\bself\\.${ref}\\.${field}\\b`).test(text),
|
|
363
|
+
)
|
|
364
|
+
: new RegExp(`\\bself\\.${field}\\b`).test(text),
|
|
365
|
+
);
|
|
366
|
+
if (own?.kind === "block" && prefix)
|
|
367
|
+
for (const entry of own.entries) {
|
|
368
|
+
const value = fieldValue(entry, spell);
|
|
369
|
+
if (reads(value)) uses.push(`${prefix}${entry.key}: ${value}`);
|
|
370
|
+
}
|
|
371
|
+
for (const entry of body.entries) {
|
|
372
|
+
if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
|
|
373
|
+
continue;
|
|
374
|
+
for (const part of entry.value.entries) {
|
|
375
|
+
if (["actor", "from", "to", "summary"].includes(part.key)) continue;
|
|
376
|
+
const text = spell(part.value);
|
|
377
|
+
// A long slot, such as an invoke list, is named rather than spelled.
|
|
378
|
+
if (reads(text))
|
|
379
|
+
uses.push(
|
|
380
|
+
`${prefix}${entry.key.slice(7)} ${part.key}${text.length > 100 ? "" : `: ${text}`}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const records = slot(body, "records");
|
|
385
|
+
if (records?.kind === "block")
|
|
386
|
+
for (const record of records.entries) {
|
|
387
|
+
if (record.value.kind !== "block") continue;
|
|
388
|
+
const recordFields = slot(record.value, "fields");
|
|
389
|
+
const refs =
|
|
390
|
+
recordFields?.kind === "block"
|
|
391
|
+
? recordFields.entries
|
|
392
|
+
.filter((entry) =>
|
|
393
|
+
/^ref<parent>/.test(fieldValue(entry, spell)),
|
|
394
|
+
)
|
|
395
|
+
.map((entry) => entry.key)
|
|
396
|
+
: [];
|
|
397
|
+
visit(record.value, `${prefix}${record.key}.`, refs);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
visit(decl.body, "", []);
|
|
401
|
+
const parts = [
|
|
402
|
+
...(fallback === "runtime"
|
|
403
|
+
? [
|
|
404
|
+
`Leave it out of the attach and create takes ${name} for each record, or bind one fixed value.`,
|
|
405
|
+
]
|
|
406
|
+
: []),
|
|
407
|
+
...(set.length
|
|
408
|
+
? [
|
|
409
|
+
`Sets ${set.map((entry) => `${entry.key}: ${fieldValue(entry, spell)}`).join(", ")}.`,
|
|
410
|
+
]
|
|
411
|
+
: []),
|
|
412
|
+
...(uses.length ? [`Read by ${uses.slice(0, 6).join("; ")}.`] : []),
|
|
413
|
+
];
|
|
414
|
+
return parts.length ? parts.join(" ") : "Configures the template.";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function fieldValue(entry: Entry, spell: (expression: Expr) => string) {
|
|
418
|
+
return entry.value.kind === "default"
|
|
419
|
+
? `${spell(entry.value.type)} = ${spell(entry.value.value)}`
|
|
420
|
+
: spell(entry.value);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
interface Example {
|
|
424
|
+
attachment: string;
|
|
425
|
+
render(exposes: string[]): string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The template's attach from the catalog program, with every attach it names
|
|
430
|
+
* and the parties and headers those need.
|
|
431
|
+
*/
|
|
432
|
+
function exampleProgram(
|
|
433
|
+
qualifiedName: string,
|
|
434
|
+
library: StandardLibrary,
|
|
435
|
+
): Example {
|
|
436
|
+
const program = parseProgram(catalogProgramSource).program;
|
|
437
|
+
const object = program.decls.find((decl) => decl.kind === "object");
|
|
438
|
+
if (object?.kind !== "object")
|
|
439
|
+
throw new Error("catalog program has no object");
|
|
440
|
+
const attaches = object.body.entries.flatMap((entry) => {
|
|
441
|
+
const match = entry.key.match(/^attach (\w+) = (\w+)\.(\w+)$/);
|
|
442
|
+
return match
|
|
443
|
+
? [
|
|
444
|
+
{
|
|
445
|
+
entry,
|
|
446
|
+
name: match[1]!,
|
|
447
|
+
header: match[2]!,
|
|
448
|
+
template: `${match[2]}.${match[3]}`,
|
|
449
|
+
},
|
|
450
|
+
]
|
|
451
|
+
: [];
|
|
452
|
+
});
|
|
453
|
+
const target = attaches.find((attach) => attach.template === qualifiedName);
|
|
454
|
+
if (!target)
|
|
455
|
+
throw new Error(`catalog program does not attach ${qualifiedName}`);
|
|
456
|
+
const referenced = (expression: Expr): string[] => {
|
|
457
|
+
if (expression.kind === "name") return [expression.value.split(".")[0]!];
|
|
458
|
+
if (expression.kind === "list") return expression.items.flatMap(referenced);
|
|
459
|
+
if (expression.kind === "block")
|
|
460
|
+
return expression.entries.flatMap((entry) => referenced(entry.value));
|
|
461
|
+
return [];
|
|
462
|
+
};
|
|
463
|
+
// A parameter that defaults to object(header.template) binds the sibling
|
|
464
|
+
// attach of that template, so the example carries that attach too.
|
|
465
|
+
const templates = new Map(
|
|
466
|
+
headerManifest(library).headers.flatMap((header) =>
|
|
467
|
+
header.objects.map((object) => [object.qualifiedName, object] as const),
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
const siblings = (template: string) =>
|
|
471
|
+
(templates.get(template)?.tunables ?? []).flatMap((tunable) => {
|
|
472
|
+
const match = tunable.default?.match(/^object\((\w+\.\w+)\)$/);
|
|
473
|
+
const sibling = match
|
|
474
|
+
? attaches.find((attach) => attach.template === match[1])
|
|
475
|
+
: undefined;
|
|
476
|
+
return sibling ? [sibling.name] : [];
|
|
477
|
+
});
|
|
478
|
+
const names = new Set<string>();
|
|
479
|
+
const include = (name: string) => {
|
|
480
|
+
if (names.has(name)) return;
|
|
481
|
+
const attach = attaches.find((candidate) => candidate.name === name);
|
|
482
|
+
if (!attach) return;
|
|
483
|
+
names.add(name);
|
|
484
|
+
[...referenced(attach.entry.value), ...siblings(attach.template)].forEach(
|
|
485
|
+
include,
|
|
486
|
+
);
|
|
487
|
+
};
|
|
488
|
+
include(target.name);
|
|
489
|
+
const included = attaches.filter((attach) => names.has(attach.name));
|
|
490
|
+
const used = new Set(
|
|
491
|
+
included.flatMap((attach) => referenced(attach.entry.value)),
|
|
492
|
+
);
|
|
493
|
+
const text = (span: { start: number; end: number }) =>
|
|
494
|
+
catalogProgramSource.slice(span.start, span.end).trim();
|
|
495
|
+
return {
|
|
496
|
+
attachment: target.name,
|
|
497
|
+
render: (exposes) =>
|
|
498
|
+
[
|
|
499
|
+
`program example "${qualifiedName} example"`,
|
|
500
|
+
...new Set(included.map((attach) => `use ${attach.header}`)),
|
|
501
|
+
...program.decls.flatMap((decl) =>
|
|
502
|
+
decl.kind === "party" && used.has(decl.name) ? [text(decl.span)] : [],
|
|
503
|
+
),
|
|
504
|
+
`object item "Item" {`,
|
|
505
|
+
...included.map((attach) => {
|
|
506
|
+
const line = text(attach.entry.span);
|
|
507
|
+
return wrapAttach(
|
|
508
|
+
attach === target && exposes.length
|
|
509
|
+
? line.replace(/\s*\}$/, `, ${exposes.join(", ")} }`)
|
|
510
|
+
: line,
|
|
511
|
+
);
|
|
512
|
+
}),
|
|
513
|
+
"}",
|
|
514
|
+
].join("\n"),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** An attach line within 76 columns, else one binding per line. */
|
|
519
|
+
function wrapAttach(line: string): string {
|
|
520
|
+
if (line.length <= 74) return ` ${line}`;
|
|
521
|
+
const open = line.indexOf("{");
|
|
522
|
+
const items: string[] = [];
|
|
523
|
+
let depth = 0;
|
|
524
|
+
let quoted = false;
|
|
525
|
+
let start = open + 1;
|
|
526
|
+
for (let index = start; index < line.length - 1; index++) {
|
|
527
|
+
const char = line[index]!;
|
|
528
|
+
if (char === '"') quoted = !quoted;
|
|
529
|
+
if (quoted) continue;
|
|
530
|
+
if ("{[(".includes(char)) depth++;
|
|
531
|
+
if ("}])".includes(char)) depth--;
|
|
532
|
+
if (char === "," && depth === 0) {
|
|
533
|
+
items.push(line.slice(start, index).trim());
|
|
534
|
+
start = index + 1;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
items.push(line.slice(start, line.lastIndexOf("}")).trim());
|
|
538
|
+
return [
|
|
539
|
+
` ${line.slice(0, open + 1)}`,
|
|
540
|
+
...items.map((item) => ` ${item},`),
|
|
541
|
+
" }",
|
|
542
|
+
].join("\n");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
type GuideOptions = Pick<CompileOptions, "adapterRegistry"> & {
|
|
546
|
+
library?: StandardLibrary;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/** The example's compiled root instrument, or the compiler's refusal. */
|
|
550
|
+
function compiledAttachment(
|
|
551
|
+
source: string,
|
|
552
|
+
attachment: string,
|
|
553
|
+
options: GuideOptions,
|
|
554
|
+
) {
|
|
555
|
+
const result = compile(source, {
|
|
556
|
+
...(options.library ? { standardLibrary: options.library } : {}),
|
|
557
|
+
...(options.adapterRegistry
|
|
558
|
+
? { adapterRegistry: options.adapterRegistry }
|
|
559
|
+
: {}),
|
|
560
|
+
});
|
|
561
|
+
const document = result.artifacts?.document;
|
|
562
|
+
if (!document)
|
|
563
|
+
throw new Error(
|
|
564
|
+
`${attachment} example does not compile: ${result.diagnostics.map((d) => d.message).join("; ")}`,
|
|
565
|
+
);
|
|
566
|
+
const instrumentId = document.objects
|
|
567
|
+
.flatMap((object) => object.attachments)
|
|
568
|
+
.find((candidate) => candidate.name === attachment)?.instrument;
|
|
569
|
+
const instrument = document.instruments.find(
|
|
570
|
+
(candidate) => candidate.id === instrumentId,
|
|
571
|
+
);
|
|
572
|
+
if (!instrument) throw new Error(`${attachment} example has no instrument`);
|
|
573
|
+
return Object.assign(instrument, { warnings: result.warnings });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function inputType(field: Record<string, unknown>) {
|
|
577
|
+
const type = String(field.type);
|
|
578
|
+
if (type === "list")
|
|
579
|
+
return `list of ${String(field.item)}${field.maxItems === undefined ? "" : `, at most ${String(field.maxItems)}`}`;
|
|
580
|
+
if (type === "ref" && field.target) return `ref to ${String(field.target)}`;
|
|
581
|
+
if (type === "enum" && Array.isArray(field.values))
|
|
582
|
+
return `one of ${field.values.join(", ")}`;
|
|
583
|
+
return type;
|
|
584
|
+
}
|