@hyperscale0/hsx 3.2.0 → 4.0.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/src/ast.d.ts +17 -2
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.js +1 -1
- package/dist/src/compile.d.ts +6 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +1194 -97
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +1 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +52 -9
- package/dist/src/cost.js.map +1 -1
- package/dist/src/headers.d.ts +2 -2
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +3 -2
- package/dist/src/headers.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lex.d.ts +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lex.js +5 -0
- package/dist/src/lex.js.map +1 -1
- package/dist/src/parse.js +91 -5
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +10 -9
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +2 -2
- package/docs/README.md +51 -27
- package/docs/headers.md +43 -40
- package/examples/cost-table.json +40 -324
- package/examples/library.hsx +12 -57
- package/package.json +7 -5
- package/src/ast.ts +11 -1
- package/src/cli.ts +1 -1
- package/src/compile.ts +1630 -114
- package/src/cost.ts +60 -16
- package/src/headers.ts +3 -2
- package/src/index.ts +12 -1
- package/src/lex.ts +5 -0
- package/src/parse.ts +87 -5
- package/src/std-bundle.ts +10 -9
- package/src/version.ts +2 -2
- package/std/approvals.hsx +3 -3
- package/std/collections.hsx +45 -4
- package/std/escrow.hsx +17 -8
- package/std/financing.hsx +292 -42
- package/std/insurance.hsx +4 -5
- package/std/lending.hsx +7 -8
- package/std/marketplace.hsx +90 -6
- package/std/money.hsx +15 -49
- package/std/reporting.hsx +1286 -0
- package/std/travel.hsx +5 -6
package/dist/src/compile.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { hash as sha256 } from "fast-sha256";
|
|
1
2
|
import { buildUdlCostManifest } from "./cost.js";
|
|
2
|
-
import { validateUdl, } from "@hyperscale0/udl";
|
|
3
|
+
import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, } from "@hyperscale0/udl";
|
|
3
4
|
import { tunableBounds } from "./tunables.js";
|
|
4
5
|
import { parseProgram } from "./parse.js";
|
|
5
6
|
import { lineColAt, } from "./ast.js";
|
|
@@ -12,7 +13,125 @@ class CompileFailure extends Error {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
function fail(expr, message, fix) {
|
|
15
|
-
|
|
16
|
+
return failWithCode(expr, "HSX1001", message, fix);
|
|
17
|
+
}
|
|
18
|
+
function failWithCode(expr, code, message, fix) {
|
|
19
|
+
throw new CompileFailure({
|
|
20
|
+
code,
|
|
21
|
+
message,
|
|
22
|
+
fix,
|
|
23
|
+
span: expr.span,
|
|
24
|
+
...(expr.source ? { source: expr.source } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function lowerFieldShape(row, resolveExpr = (e) => e) {
|
|
28
|
+
let rawValue = row.value.kind === "default" ? row.value.type : row.value;
|
|
29
|
+
let isSensitive = false;
|
|
30
|
+
if (rawValue.kind === "call" && rawValue.name === "sensitive") {
|
|
31
|
+
isSensitive = true;
|
|
32
|
+
rawValue = rawValue.args[0];
|
|
33
|
+
}
|
|
34
|
+
const t = rawValue;
|
|
35
|
+
if (t.kind === "block") {
|
|
36
|
+
const b = entries(t);
|
|
37
|
+
if (b.has("family") || b.has("target") || b.has("instrument")) {
|
|
38
|
+
const tgt = b.get("instrument") ?? b.get("target");
|
|
39
|
+
let target;
|
|
40
|
+
if (tgt) {
|
|
41
|
+
const resolved = resolveExpr(tgt);
|
|
42
|
+
if (resolved.kind === "list") {
|
|
43
|
+
const items = resolved.items.map((i) => text(resolveExpr(i)));
|
|
44
|
+
target = items.length === 1 ? items[0] : items;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
target = text(resolved);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: row.key,
|
|
52
|
+
type: "ref",
|
|
53
|
+
targetKind: "instrument",
|
|
54
|
+
...(target !== undefined ? { target } : {}),
|
|
55
|
+
...(isSensitive ? { sensitive: true } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
60
|
+
const f = {
|
|
61
|
+
name: row.key,
|
|
62
|
+
type,
|
|
63
|
+
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
64
|
+
...(isSensitive ? { sensitive: true } : {}),
|
|
65
|
+
};
|
|
66
|
+
if (type === "enum" && t.kind === "call") {
|
|
67
|
+
f.values = t.args.map(text);
|
|
68
|
+
}
|
|
69
|
+
if (type === "text" && t.kind === "call") {
|
|
70
|
+
if (t.args.length < 2 || t.args.length > 3) {
|
|
71
|
+
fail(t, "bounded text needs length bounds and an optional pattern", "write text(1, 80)");
|
|
72
|
+
}
|
|
73
|
+
f.minLength = literal(resolveExpr(t.args[0]));
|
|
74
|
+
f.maxLength = literal(resolveExpr(t.args[1]));
|
|
75
|
+
if (t.args[2])
|
|
76
|
+
f.pattern = literal(resolveExpr(t.args[2]));
|
|
77
|
+
}
|
|
78
|
+
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
79
|
+
if (t.args.length !== 2) {
|
|
80
|
+
fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
|
|
81
|
+
}
|
|
82
|
+
f.minimum = literal(resolveExpr(t.args[0]));
|
|
83
|
+
f.maximum = literal(resolveExpr(t.args[1]));
|
|
84
|
+
}
|
|
85
|
+
if (type === "list" && t.kind === "call") {
|
|
86
|
+
const item = t.args[0];
|
|
87
|
+
f.item = item.kind === "type" ? item.name : text(item);
|
|
88
|
+
if (item.kind === "type" && item.name === "ref") {
|
|
89
|
+
f.target = item.target;
|
|
90
|
+
f.targetKind = "object";
|
|
91
|
+
}
|
|
92
|
+
f.maxItems = t.args[1] ? literal(resolveExpr(t.args[1])) : 366;
|
|
93
|
+
}
|
|
94
|
+
if (type === "list" && t.kind === "type") {
|
|
95
|
+
f.item = t.target;
|
|
96
|
+
f.maxItems = 366;
|
|
97
|
+
}
|
|
98
|
+
if (type === "ref") {
|
|
99
|
+
const target = t.kind === "type" ? t.target : f.target;
|
|
100
|
+
if (!target && t.kind !== "block") {
|
|
101
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
102
|
+
}
|
|
103
|
+
f.targetKind = f.targetKind ?? "object";
|
|
104
|
+
if (target)
|
|
105
|
+
f.target = target;
|
|
106
|
+
}
|
|
107
|
+
return f;
|
|
108
|
+
}
|
|
109
|
+
function lowerObjectField(row, resolveExpr = (e) => e) {
|
|
110
|
+
const f = lowerFieldShape(row, resolveExpr);
|
|
111
|
+
const constant = row.value.kind === "default" ? resolveExpr(row.value.value) : undefined;
|
|
112
|
+
if (constant && !(constant.kind === "name" && constant.value === "runtime")) {
|
|
113
|
+
f.value =
|
|
114
|
+
f.type === "enum" && constant.kind === "name"
|
|
115
|
+
? constant.value
|
|
116
|
+
: literal(constant);
|
|
117
|
+
}
|
|
118
|
+
const result = udlObjectFieldSchema.safeParse(f);
|
|
119
|
+
if (!result.success)
|
|
120
|
+
fail(row, result.error.message, "use a UDL object field type and its constraints");
|
|
121
|
+
return result.data;
|
|
122
|
+
}
|
|
123
|
+
function canonicalJson(value) {
|
|
124
|
+
if (value === null || typeof value !== "object") {
|
|
125
|
+
return JSON.stringify(value);
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
|
|
129
|
+
}
|
|
130
|
+
const record = value;
|
|
131
|
+
const keys = Object.keys(record)
|
|
132
|
+
.filter((k) => record[k] !== undefined)
|
|
133
|
+
.sort();
|
|
134
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
|
|
16
135
|
}
|
|
17
136
|
const emptyBlock = {
|
|
18
137
|
kind: "block",
|
|
@@ -28,7 +147,14 @@ function entries(block) {
|
|
|
28
147
|
for (const row of block.entries) {
|
|
29
148
|
const previous = map.get(row.key);
|
|
30
149
|
if (previous) {
|
|
31
|
-
if (![
|
|
150
|
+
if (![
|
|
151
|
+
"requires",
|
|
152
|
+
"moves",
|
|
153
|
+
"invariants",
|
|
154
|
+
"invoke",
|
|
155
|
+
"calculate",
|
|
156
|
+
"expose",
|
|
157
|
+
].includes(row.key))
|
|
32
158
|
fail(row, `duplicate ${row.key}`, "keep one value for this name");
|
|
33
159
|
const items = (expr) => expr.kind === "list" ? expr.items : [expr];
|
|
34
160
|
map.set(row.key, {
|
|
@@ -126,9 +252,10 @@ function literal(expr) {
|
|
|
126
252
|
}
|
|
127
253
|
export function compile(source, options = {}) {
|
|
128
254
|
const parsed = parseProgram(source);
|
|
255
|
+
const sources = new Map([["program", source]]);
|
|
129
256
|
const diagnostic = (d, stage) => ({
|
|
130
257
|
...d,
|
|
131
|
-
...lineColAt(source, d.span.start),
|
|
258
|
+
...lineColAt(sources.get(d.source ?? "program") ?? source, d.span.start),
|
|
132
259
|
severity: "error",
|
|
133
260
|
stage,
|
|
134
261
|
});
|
|
@@ -144,6 +271,9 @@ export function compile(source, options = {}) {
|
|
|
144
271
|
if (program.currency !== "SAR")
|
|
145
272
|
fail(program, "this release supports SAR", "write currency SAR or omit currency");
|
|
146
273
|
const templates = new Map();
|
|
274
|
+
const declarationSources = new Map();
|
|
275
|
+
const declarationExportPaths = new Map();
|
|
276
|
+
const requirementOrigins = new Map();
|
|
147
277
|
const used = new Set();
|
|
148
278
|
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
149
279
|
if (used.has(use.name))
|
|
@@ -152,17 +282,38 @@ export function compile(source, options = {}) {
|
|
|
152
282
|
const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
|
|
153
283
|
if (!content)
|
|
154
284
|
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
285
|
+
sources.set(use.name, content);
|
|
155
286
|
const header = parseProgram(content);
|
|
156
287
|
if (header.diagnostics.length ||
|
|
157
288
|
!header.program.header ||
|
|
158
289
|
header.program.name !== use.name)
|
|
159
290
|
fail(use, `header ${use.name} is malformed`, "repair the header source before compiling");
|
|
291
|
+
const registerTemplates = (parentDecl, prefix, exportPath) => {
|
|
292
|
+
templates.set(prefix, parentDecl);
|
|
293
|
+
declarationSources.set(parentDecl, use.name);
|
|
294
|
+
declarationExportPaths.set(parentDecl, exportPath);
|
|
295
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
296
|
+
for (const [recName, recBlock] of recs) {
|
|
297
|
+
const recDecl = {
|
|
298
|
+
kind: "instrument",
|
|
299
|
+
name: recName,
|
|
300
|
+
parameters: [],
|
|
301
|
+
body: asBlock(recBlock),
|
|
302
|
+
span: parentDecl.span,
|
|
303
|
+
};
|
|
304
|
+
registerTemplates(recDecl, `${prefix}.${recName}`, `${exportPath}.${recName}`);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
160
307
|
for (const decl of header.program.decls)
|
|
161
|
-
if (decl.kind === "instrument")
|
|
162
|
-
|
|
308
|
+
if (decl.kind === "instrument") {
|
|
309
|
+
registerTemplates(decl, `${use.name}.${decl.name}`, decl.name);
|
|
310
|
+
}
|
|
163
311
|
}
|
|
312
|
+
for (const decl of program.decls)
|
|
313
|
+
if (decl.kind === "instrument")
|
|
314
|
+
templates.set(decl.name, decl);
|
|
164
315
|
const document = {
|
|
165
|
-
udl:
|
|
316
|
+
udl: 4,
|
|
166
317
|
version: 1,
|
|
167
318
|
product: program.name,
|
|
168
319
|
title: program.title,
|
|
@@ -173,9 +324,12 @@ export function compile(source, options = {}) {
|
|
|
173
324
|
programFines: { kind: "business", role: "fine_payable" },
|
|
174
325
|
programCosts: { kind: "business", role: "cost_recovery" },
|
|
175
326
|
},
|
|
327
|
+
objects: [],
|
|
176
328
|
instruments: [],
|
|
177
329
|
};
|
|
178
330
|
const objects = new Map();
|
|
331
|
+
const assignments = new Map();
|
|
332
|
+
const attachmentSubjects = new Map();
|
|
179
333
|
const names = new Set();
|
|
180
334
|
for (const decl of program.decls) {
|
|
181
335
|
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
@@ -183,9 +337,28 @@ export function compile(source, options = {}) {
|
|
|
183
337
|
if (names.has(decl.name))
|
|
184
338
|
fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
|
|
185
339
|
names.add(decl.name);
|
|
186
|
-
if (decl.kind === "object")
|
|
340
|
+
if (decl.kind === "object") {
|
|
187
341
|
objects.set(decl.name, decl);
|
|
342
|
+
for (const entry of decl.body.entries) {
|
|
343
|
+
const match = /^attach\s+(\w+)\s*=\s*(.+)$/.exec(entry.key);
|
|
344
|
+
if (!match)
|
|
345
|
+
continue;
|
|
346
|
+
const name = `${decl.name}_${match[1]}`;
|
|
347
|
+
assignments.set(name, {
|
|
348
|
+
kind: "assignment",
|
|
349
|
+
name,
|
|
350
|
+
target: match[2],
|
|
351
|
+
body: asBlock(entry.value),
|
|
352
|
+
span: entry.span,
|
|
353
|
+
});
|
|
354
|
+
attachmentSubjects.set(name, decl.name);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (decl.kind === "assignment")
|
|
358
|
+
assignments.set(decl.name, decl);
|
|
188
359
|
if (decl.kind === "party") {
|
|
360
|
+
if (subjectPartyRoles.includes(decl.name))
|
|
361
|
+
failWithCode(decl, "party_name_reserved", `${decl.name} is a reserved subject role`, "choose a party name other than owner, actor or operator");
|
|
189
362
|
if (!["person", "business", "staff"].includes(decl.partyKind))
|
|
190
363
|
fail(decl, `unknown party kind ${decl.partyKind}`, "choose person, business, or staff");
|
|
191
364
|
document.parties[decl.name] = {
|
|
@@ -197,7 +370,170 @@ export function compile(source, options = {}) {
|
|
|
197
370
|
const origins = [];
|
|
198
371
|
const materialApprovals = new Set();
|
|
199
372
|
const implicitDecisions = new Map();
|
|
200
|
-
const
|
|
373
|
+
const resolveFamily = (rawPath, expr, required = true) => {
|
|
374
|
+
const parts = rawPath.split(".");
|
|
375
|
+
if (parts.length < 2) {
|
|
376
|
+
if (!required)
|
|
377
|
+
return undefined;
|
|
378
|
+
failWithCode(expr, "HSX1001", `invalid family ${rawPath}`, "use module.instrument or module.instrument.record");
|
|
379
|
+
}
|
|
380
|
+
const moduleName = parts[0];
|
|
381
|
+
const exportPath = parts.slice(1).join(".");
|
|
382
|
+
const targetTemplate = templates.get(rawPath);
|
|
383
|
+
if (!targetTemplate) {
|
|
384
|
+
if (!required)
|
|
385
|
+
return undefined;
|
|
386
|
+
failWithCode(expr, "HSX1001", `unknown family declaration ${rawPath}`, "choose a declared standard instrument");
|
|
387
|
+
}
|
|
388
|
+
const topTemplate = templates.get(`${moduleName}.${parts[1]}`);
|
|
389
|
+
if (!topTemplate) {
|
|
390
|
+
if (!required)
|
|
391
|
+
return undefined;
|
|
392
|
+
failWithCode(expr, "HSX1001", `unknown family declaration ${moduleName}.${parts[1]}`, "choose a declared standard instrument");
|
|
393
|
+
}
|
|
394
|
+
const topBody = entries(topTemplate.body);
|
|
395
|
+
let revision;
|
|
396
|
+
if (topBody.has("familyRevision")) {
|
|
397
|
+
const val = literal(topBody.get("familyRevision"));
|
|
398
|
+
if (typeof val === "number")
|
|
399
|
+
revision = val;
|
|
400
|
+
}
|
|
401
|
+
let currentBody = topTemplate.body;
|
|
402
|
+
for (const recName of parts.slice(2)) {
|
|
403
|
+
const recs = entries(asBlock(entries(currentBody).get("records")));
|
|
404
|
+
const child = recs.get(recName);
|
|
405
|
+
if (child) {
|
|
406
|
+
currentBody = asBlock(child);
|
|
407
|
+
const cBody = entries(currentBody);
|
|
408
|
+
if (cBody.has("familyRevision")) {
|
|
409
|
+
const val = literal(cBody.get("familyRevision"));
|
|
410
|
+
if (typeof val === "number")
|
|
411
|
+
revision = val;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (revision === undefined) {
|
|
416
|
+
if (!required)
|
|
417
|
+
return undefined;
|
|
418
|
+
failWithCode(expr, "HSX1001", `declaration ${rawPath} has no familyRevision declared`, "declare familyRevision on the standard instrument");
|
|
419
|
+
}
|
|
420
|
+
return { module: moduleName, exportPath, revision };
|
|
421
|
+
};
|
|
422
|
+
const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedApprovers = new Set(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration) => {
|
|
423
|
+
const resolveFamilyInstruments = (family, currentInstId, expr) => {
|
|
424
|
+
const found = new Set();
|
|
425
|
+
if (currentInstId && familyDeclaration) {
|
|
426
|
+
if (family.module === familyDeclaration.module &&
|
|
427
|
+
family.exportPath.startsWith(`${familyDeclaration.exportPath}.`)) {
|
|
428
|
+
const sub = family.exportPath
|
|
429
|
+
.slice(familyDeclaration.exportPath.length + 1)
|
|
430
|
+
.replaceAll(".", "_");
|
|
431
|
+
found.add(`${currentInstId}_${sub}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const topDeclName = family.exportPath.split(".")[0];
|
|
435
|
+
const subRecordPath = family.exportPath.includes(".")
|
|
436
|
+
? family.exportPath.slice(topDeclName.length + 1).replaceAll(".", "_")
|
|
437
|
+
: undefined;
|
|
438
|
+
for (const asgn of assignments.values()) {
|
|
439
|
+
if (asgn.target === `${family.module}.${topDeclName}`) {
|
|
440
|
+
if (subRecordPath) {
|
|
441
|
+
found.add(`${asgn.name}_${subRecordPath}`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
found.add(asgn.name);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const inst of document.instruments) {
|
|
449
|
+
if (inst.family &&
|
|
450
|
+
inst.family.module === family.module &&
|
|
451
|
+
inst.family.exportPath === family.exportPath &&
|
|
452
|
+
inst.family.revision === family.revision) {
|
|
453
|
+
found.add(inst.id);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const result = [...found];
|
|
457
|
+
if (result.length === 0 && expr) {
|
|
458
|
+
failWithCode(expr, "HSX1001", `no instruments found for family ${family.module}.${family.exportPath}`, "declare an attachment matching this family");
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
};
|
|
462
|
+
const resolveChildExportPath = (parentDecl, suffix) => {
|
|
463
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
464
|
+
for (const [recName] of recs) {
|
|
465
|
+
if (suffix === recName)
|
|
466
|
+
return recName;
|
|
467
|
+
}
|
|
468
|
+
for (const [recName, recBlock] of recs) {
|
|
469
|
+
if (suffix.startsWith(`${recName}_`)) {
|
|
470
|
+
const rest = resolveChildExportPath({
|
|
471
|
+
kind: "instrument",
|
|
472
|
+
name: recName,
|
|
473
|
+
parameters: [],
|
|
474
|
+
body: asBlock(recBlock),
|
|
475
|
+
span: parentDecl.span,
|
|
476
|
+
}, suffix.slice(recName.length + 1));
|
|
477
|
+
if (rest)
|
|
478
|
+
return `${recName}.${rest}`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
};
|
|
483
|
+
const getInstrumentFamily = (targetId) => {
|
|
484
|
+
const existing = document.instruments.find((i) => i.id === targetId);
|
|
485
|
+
if (existing?.family)
|
|
486
|
+
return existing.family;
|
|
487
|
+
if (targetId.startsWith(`${id}_`) && familyDeclaration) {
|
|
488
|
+
const sub = resolveChildExportPath(decl, targetId.slice(id.length + 1));
|
|
489
|
+
if (sub) {
|
|
490
|
+
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
491
|
+
try {
|
|
492
|
+
return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
|
|
493
|
+
}
|
|
494
|
+
catch { }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const [asgnName, asgn] of assignments) {
|
|
498
|
+
if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
|
|
499
|
+
const tmpl = templates.get(asgn.target);
|
|
500
|
+
if (!tmpl)
|
|
501
|
+
continue;
|
|
502
|
+
const mod = declarationSources.get(tmpl);
|
|
503
|
+
if (!mod || mod === "program")
|
|
504
|
+
continue;
|
|
505
|
+
if (targetId === asgnName) {
|
|
506
|
+
try {
|
|
507
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
508
|
+
}
|
|
509
|
+
catch { }
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
const sub = resolveChildExportPath(tmpl, targetId.slice(asgnName.length + 1));
|
|
513
|
+
if (sub) {
|
|
514
|
+
const fullTarget = `${asgn.target}.${sub}`;
|
|
515
|
+
try {
|
|
516
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
517
|
+
}
|
|
518
|
+
catch { }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
};
|
|
525
|
+
const checkTargetFamily = (targetIds, expectedFamily, expr) => {
|
|
526
|
+
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
527
|
+
for (const tid of ids) {
|
|
528
|
+
const fam = getInstrumentFamily(tid);
|
|
529
|
+
if (!fam ||
|
|
530
|
+
fam.module !== expectedFamily.module ||
|
|
531
|
+
fam.exportPath !== expectedFamily.exportPath ||
|
|
532
|
+
fam.revision !== expectedFamily.revision) {
|
|
533
|
+
failWithCode(expr, "HSX1001", `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`, "ensure target instrument matches the declared family");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
201
537
|
const enums = new Map(inheritedEnums);
|
|
202
538
|
for (const parameter of decl.parameters) {
|
|
203
539
|
const type = parameter.value.kind === "default"
|
|
@@ -207,23 +543,44 @@ export function compile(source, options = {}) {
|
|
|
207
543
|
enums.set(parameter.key, type.args.map(text));
|
|
208
544
|
}
|
|
209
545
|
const approvers = new Set(inheritedApprovers);
|
|
210
|
-
const supplied =
|
|
546
|
+
const supplied = new Map();
|
|
547
|
+
for (const entry of arguments_.entries) {
|
|
548
|
+
if (supplied.has(entry.key))
|
|
549
|
+
fail(entry, `duplicate tunable ${entry.key}`, "supply each parameter once");
|
|
550
|
+
supplied.set(entry.key, entry.value);
|
|
551
|
+
}
|
|
211
552
|
const environment = new Map(inherited);
|
|
212
553
|
for (const param of decl.parameters) {
|
|
213
554
|
const type = param.value.kind === "default" ? param.value.type : param.value;
|
|
214
555
|
const fallback = param.value.kind === "default" ? param.value.value : undefined;
|
|
215
|
-
const
|
|
556
|
+
const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
|
|
557
|
+
const partyParameter = attachmentInfo && (typeName === "party" || typeName === "approval");
|
|
558
|
+
const byName = partyParameter &&
|
|
559
|
+
(subjectPartyRoles.includes(param.key) ||
|
|
560
|
+
document.parties[param.key])
|
|
561
|
+
? { kind: "name", value: param.key, span: origin }
|
|
562
|
+
: undefined;
|
|
563
|
+
const actual = supplied.get(param.key) ??
|
|
564
|
+
byName ??
|
|
565
|
+
(fallback && {
|
|
566
|
+
...fallback,
|
|
567
|
+
source: declarationSources.get(decl) ?? "program",
|
|
568
|
+
});
|
|
216
569
|
if (!actual) {
|
|
217
570
|
if (type.kind === "type" && type.optional)
|
|
218
571
|
continue;
|
|
219
|
-
|
|
572
|
+
failWithCode({ span: origin }, partyParameter ? "subject_party_unbound" : "HSX1001", `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
|
|
220
573
|
}
|
|
221
574
|
environment.set(param.key, actual);
|
|
222
575
|
}
|
|
223
576
|
for (const key of supplied.keys())
|
|
224
577
|
if (!decl.parameters.some((p) => p.key === key))
|
|
225
578
|
fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
|
|
226
|
-
const
|
|
579
|
+
const isParty = (name) => !!document.parties[name] ||
|
|
580
|
+
(!!attachmentInfo &&
|
|
581
|
+
subjectPartyRoles.includes(name));
|
|
582
|
+
const resolvedParties = new Set();
|
|
583
|
+
const resolve = (expr, seen = new Set(), partyBinding = false) => {
|
|
227
584
|
if (expr.kind === "call" &&
|
|
228
585
|
["object", "all", "party"].includes(expr.name)) {
|
|
229
586
|
if (expr.args.length !== 1)
|
|
@@ -231,15 +588,19 @@ export function compile(source, options = {}) {
|
|
|
231
588
|
const type = text(expr.args[0]);
|
|
232
589
|
const matches = expr.name === "party"
|
|
233
590
|
? Object.entries(document.parties)
|
|
234
|
-
.filter(([, party]) => party.kind === type)
|
|
591
|
+
.filter(([name, party]) => party.kind === type && (!partyBinding || names.has(name)))
|
|
235
592
|
.map(([name]) => name)
|
|
236
|
-
: [...
|
|
237
|
-
.filter((
|
|
238
|
-
|
|
239
|
-
.
|
|
240
|
-
|
|
593
|
+
: [...assignments.values()]
|
|
594
|
+
.filter((assignment) => expr.name === "all" ||
|
|
595
|
+
!attachmentSubjects.has(assignment.name) ||
|
|
596
|
+
attachmentSubjects.get(assignment.name) ===
|
|
597
|
+
attachmentInfo?.subjectKindId)
|
|
598
|
+
.filter((assignment) => assignment.target === type ||
|
|
599
|
+
type.startsWith(`${assignment.target}.`))
|
|
600
|
+
.map((assignment) => assignment.name +
|
|
601
|
+
type.slice(assignment.target.length).replaceAll(".", "_"));
|
|
241
602
|
if (expr.name !== "all" && matches.length !== 1)
|
|
242
|
-
|
|
603
|
+
failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
|
|
243
604
|
const items = matches.map((value) => ({
|
|
244
605
|
kind: "name",
|
|
245
606
|
value,
|
|
@@ -251,27 +612,45 @@ export function compile(source, options = {}) {
|
|
|
251
612
|
}
|
|
252
613
|
if (expr.kind !== "name")
|
|
253
614
|
return expr;
|
|
615
|
+
const binding = environment.get(expr.value);
|
|
616
|
+
if (attachmentInfo &&
|
|
617
|
+
subjectPartyRoles.includes(expr.value) &&
|
|
618
|
+
(!binding ||
|
|
619
|
+
(binding.kind === "name" && binding.value === expr.value)))
|
|
620
|
+
return expr;
|
|
621
|
+
if (attachmentInfo) {
|
|
622
|
+
const [local, ...tail] = expr.value.split(".");
|
|
623
|
+
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
624
|
+
if (attachmentSubjects.has(target))
|
|
625
|
+
return { ...expr, value: [target, ...tail].join("_") };
|
|
626
|
+
}
|
|
254
627
|
if (expr.value.startsWith("party.")) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
628
|
+
const [, party, ...members] = expr.value.split(".");
|
|
629
|
+
const binding = environment.get(party);
|
|
630
|
+
if (binding?.kind === "name" && isParty(binding.value))
|
|
631
|
+
return {
|
|
632
|
+
...expr,
|
|
633
|
+
value: ["party", binding.value, ...members].join("."),
|
|
634
|
+
};
|
|
258
635
|
}
|
|
259
636
|
const [root, ...tail] = expr.value.split(".");
|
|
260
637
|
const bound = environment.get(root);
|
|
261
638
|
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
262
639
|
return expr;
|
|
263
640
|
if (seen.has(root))
|
|
264
|
-
return
|
|
265
|
-
let resolved =
|
|
641
|
+
return failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
|
|
642
|
+
let resolved = resolvedParties.has(root) ||
|
|
643
|
+
(!partyBinding &&
|
|
644
|
+
(supplied.has(root) || inherited.has(root) || enums.has(root)))
|
|
266
645
|
? bound
|
|
267
|
-
: resolve(bound, new Set([...seen, root]));
|
|
646
|
+
: resolve(bound, new Set([...seen, root]), partyBinding);
|
|
268
647
|
for (const key of tail) {
|
|
269
648
|
if (resolved.kind !== "block")
|
|
270
649
|
return expr;
|
|
271
650
|
const child = entries(resolved).get(key);
|
|
272
651
|
if (!child)
|
|
273
652
|
return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
|
|
274
|
-
resolved = resolve(child, new Set([...seen, root]));
|
|
653
|
+
resolved = resolve(child, new Set([...seen, root]), partyBinding);
|
|
275
654
|
}
|
|
276
655
|
return resolved;
|
|
277
656
|
};
|
|
@@ -281,7 +660,9 @@ export function compile(source, options = {}) {
|
|
|
281
660
|
continue;
|
|
282
661
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
283
662
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
284
|
-
const v = supplied.has(param.key) || type === "enum"
|
|
663
|
+
const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
664
|
+
? actual
|
|
665
|
+
: resolve(actual, new Set(), !!attachmentInfo && (type === "party" || type === "approval"));
|
|
285
666
|
environment.set(param.key, v);
|
|
286
667
|
if (type === "enum" && t.kind === "call") {
|
|
287
668
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -292,8 +673,21 @@ export function compile(source, options = {}) {
|
|
|
292
673
|
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
293
674
|
}
|
|
294
675
|
else if (type === "party" || type === "approval") {
|
|
295
|
-
if (v.kind !== "name" || !
|
|
296
|
-
|
|
676
|
+
if (v.kind !== "name" || !isParty(v.value))
|
|
677
|
+
failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", `${param.key} needs a declared party`, "declare a party and use its name here");
|
|
678
|
+
const party = document.parties[v.value];
|
|
679
|
+
if ((type === "approval" && (party?.kind !== "staff" || !party.role)) ||
|
|
680
|
+
(type === "party" &&
|
|
681
|
+
(party?.kind === "staff" ||
|
|
682
|
+
(attachmentInfo && party?.kind === "person"))))
|
|
683
|
+
failWithCode(actual, "party_kind_mismatch", `${param.key} cannot bind ${v.value}`, type === "approval"
|
|
684
|
+
? "use a declared staff party with a role"
|
|
685
|
+
: "use a subject role or declared business");
|
|
686
|
+
resolvedParties.add(param.key);
|
|
687
|
+
if (attachmentInfo)
|
|
688
|
+
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
|
|
689
|
+
? { role: v.value }
|
|
690
|
+
: { party: v.value };
|
|
297
691
|
if (type === "approval")
|
|
298
692
|
approvers.add(text(v));
|
|
299
693
|
}
|
|
@@ -309,11 +703,14 @@ export function compile(source, options = {}) {
|
|
|
309
703
|
fail(value, "reference needs an object name", "name a declared object");
|
|
310
704
|
const [root, ...tail] = value.value.split(".");
|
|
311
705
|
const obj = objects.get(root);
|
|
706
|
+
const assignment = assignments.get(root);
|
|
707
|
+
const targetType = obj ? obj.name : assignment?.target;
|
|
312
708
|
if ((!obj &&
|
|
709
|
+
!assignment &&
|
|
313
710
|
!document.instruments.some((inst) => inst.id === value.value)) ||
|
|
314
711
|
(t.kind === "type" &&
|
|
315
712
|
t.target &&
|
|
316
|
-
[
|
|
713
|
+
[targetType, ...tail].join(".") !== t.target))
|
|
317
714
|
fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
|
|
318
715
|
if (seen.has(value.value))
|
|
319
716
|
fail(value, "duplicate reference", "list each object once");
|
|
@@ -373,15 +770,30 @@ export function compile(source, options = {}) {
|
|
|
373
770
|
}
|
|
374
771
|
for (const key of body.keys())
|
|
375
772
|
if (![
|
|
773
|
+
"familyRevision",
|
|
376
774
|
"fields",
|
|
377
775
|
"lifecycle",
|
|
378
776
|
"records",
|
|
379
777
|
"summary",
|
|
380
778
|
"invariants",
|
|
381
779
|
"constraints",
|
|
780
|
+
"reports",
|
|
781
|
+
"revisioned",
|
|
382
782
|
].includes(key) &&
|
|
383
783
|
!key.startsWith("action "))
|
|
384
784
|
fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
|
|
785
|
+
if (body.has("familyRevision")) {
|
|
786
|
+
const revExpr = body.get("familyRevision");
|
|
787
|
+
const revVal = literal(revExpr);
|
|
788
|
+
if (typeof revVal !== "number" ||
|
|
789
|
+
!Number.isInteger(revVal) ||
|
|
790
|
+
revVal <= 0) {
|
|
791
|
+
fail(revExpr, "familyRevision must be a positive integer", "use a positive integer revision");
|
|
792
|
+
}
|
|
793
|
+
if (familyDeclaration) {
|
|
794
|
+
familyDeclaration.revision = revVal;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
385
797
|
const records = entries(asBlock(body.get("records")));
|
|
386
798
|
if (!inherited.size)
|
|
387
799
|
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
@@ -394,12 +806,54 @@ export function compile(source, options = {}) {
|
|
|
394
806
|
});
|
|
395
807
|
const fields = [];
|
|
396
808
|
const calculations = [];
|
|
809
|
+
let currentAction;
|
|
810
|
+
let currentActionName;
|
|
397
811
|
const path = (expr) => {
|
|
398
812
|
const value = resolve(expr);
|
|
399
813
|
const name = text(value);
|
|
400
|
-
if (
|
|
814
|
+
if (isParty(name))
|
|
401
815
|
return `party.${name}`;
|
|
402
|
-
|
|
816
|
+
if (name.startsWith("subject.")) {
|
|
817
|
+
const subField = name.split(".")[1];
|
|
818
|
+
if (currentAction) {
|
|
819
|
+
const req = currentAction.subject?.requirements.find((r) => r.field.name === subField);
|
|
820
|
+
if (!req) {
|
|
821
|
+
failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${currentActionName}`, `declare ${subField} in subject { ... }`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
const declaredInAction = decl.body.entries.some((e) => {
|
|
826
|
+
if (!e.key.startsWith("action "))
|
|
827
|
+
return false;
|
|
828
|
+
const subBlock = entries(asBlock(e.value)).get("subject");
|
|
829
|
+
if (!subBlock)
|
|
830
|
+
return false;
|
|
831
|
+
return asBlock(subBlock).entries.some((se) => {
|
|
832
|
+
if (se.key === subField)
|
|
833
|
+
return true;
|
|
834
|
+
if (se.key === "adapter") {
|
|
835
|
+
const names = se.value.kind === "list"
|
|
836
|
+
? se.value.items.map(text)
|
|
837
|
+
: [text(se.value)];
|
|
838
|
+
return names.some((n) => {
|
|
839
|
+
const reg = options.adapterRegistry?.[n];
|
|
840
|
+
if (!reg)
|
|
841
|
+
return false;
|
|
842
|
+
const op = reg.adapter.operationMap[reg.operation];
|
|
843
|
+
return op?.subjectRequirements?.some((sr) => sr.name === subField);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
if (!declaredInAction) {
|
|
850
|
+
failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in instrument ${id}`, `declare ${subField} in an action subject { ... }`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return /^(self|input|party|subject)\./.test(name)
|
|
855
|
+
? name
|
|
856
|
+
: `self.${name}`;
|
|
403
857
|
};
|
|
404
858
|
const val = (expr) => {
|
|
405
859
|
const v = resolve(expr);
|
|
@@ -414,7 +868,44 @@ export function compile(source, options = {}) {
|
|
|
414
868
|
return id;
|
|
415
869
|
const value = resolve(expr);
|
|
416
870
|
if (value.kind === "block") {
|
|
417
|
-
const
|
|
871
|
+
const rawEntries = entries(value);
|
|
872
|
+
if ((rawEntries.has("family") && !rawEntries.has("kind")) ||
|
|
873
|
+
((rawEntries.has("states") ||
|
|
874
|
+
rawEntries.has("reference") ||
|
|
875
|
+
rawEntries.has("anchor")) &&
|
|
876
|
+
rawEntries.has("instrument"))) {
|
|
877
|
+
let famTuple;
|
|
878
|
+
if (rawEntries.has("family")) {
|
|
879
|
+
const famExpr = rawEntries.get("family");
|
|
880
|
+
const famStr = famExpr.kind === "name" ? famExpr.value : text(famExpr);
|
|
881
|
+
famTuple = resolveFamily(famStr, famExpr);
|
|
882
|
+
}
|
|
883
|
+
let instrumentVal;
|
|
884
|
+
if (rawEntries.has("instrument")) {
|
|
885
|
+
instrumentVal = data(rawEntries.get("instrument"));
|
|
886
|
+
if (famTuple) {
|
|
887
|
+
checkTargetFamily(instrumentVal, famTuple, rawEntries.get("instrument"));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else if (famTuple) {
|
|
891
|
+
const matched = resolveFamilyInstruments(famTuple, id, rawEntries.get("family"));
|
|
892
|
+
instrumentVal = matched.length === 1 ? matched[0] : matched;
|
|
893
|
+
}
|
|
894
|
+
const result = {};
|
|
895
|
+
if (famTuple) {
|
|
896
|
+
result.family = famTuple;
|
|
897
|
+
}
|
|
898
|
+
if (instrumentVal !== undefined) {
|
|
899
|
+
result.instrument = instrumentVal;
|
|
900
|
+
}
|
|
901
|
+
for (const [k, v] of rawEntries) {
|
|
902
|
+
if (k === "family" || k === "instrument")
|
|
903
|
+
continue;
|
|
904
|
+
result[k] = data(v);
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
|
|
418
909
|
const selection = result.selection;
|
|
419
910
|
if (Array.isArray(selection?.instrument) &&
|
|
420
911
|
selection.instrument.length === 0) {
|
|
@@ -457,48 +948,77 @@ export function compile(source, options = {}) {
|
|
|
457
948
|
for (const row of block.entries) {
|
|
458
949
|
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
459
950
|
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
name: row.key,
|
|
463
|
-
type,
|
|
464
|
-
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
465
|
-
};
|
|
466
|
-
if (type === "enum" && t.kind === "call")
|
|
467
|
-
f.values = t.args.map(text);
|
|
468
|
-
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
469
|
-
if (t.args.length !== 2)
|
|
470
|
-
fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
|
|
471
|
-
f.minimum = literal(resolve(t.args[0]));
|
|
472
|
-
f.maximum = literal(resolve(t.args[1]));
|
|
473
|
-
}
|
|
951
|
+
const f = lowerFieldShape(row, resolve);
|
|
952
|
+
const type = f.type;
|
|
474
953
|
if (type === "list" && t.kind === "call") {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
954
|
+
const item = t.args[0];
|
|
955
|
+
if (item.kind === "type" && item.name === "ref" && item.target) {
|
|
956
|
+
f.target = text(resolve({ kind: "name", value: item.target, span: item.span })).replaceAll(".", "_");
|
|
957
|
+
f.targetKind = objects.has(String(f.target))
|
|
958
|
+
? "object"
|
|
959
|
+
: "instrument";
|
|
960
|
+
}
|
|
481
961
|
}
|
|
482
962
|
if (type === "ref") {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
?
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
963
|
+
if (t.kind === "block") {
|
|
964
|
+
const b = entries(t);
|
|
965
|
+
const famNode = b.get("targetFamily") ?? b.get("family");
|
|
966
|
+
let targetFamTuple;
|
|
967
|
+
if (famNode) {
|
|
968
|
+
const famStr = famNode.kind === "name" ? famNode.value : text(famNode);
|
|
969
|
+
targetFamTuple = resolveFamily(famStr, famNode);
|
|
970
|
+
f.targetFamily = targetFamTuple;
|
|
971
|
+
}
|
|
972
|
+
if (b.has("target") || b.has("instrument")) {
|
|
973
|
+
const tgtExpr = (b.get("target") ?? b.get("instrument"));
|
|
974
|
+
const resolvedTgt = resolve(tgtExpr);
|
|
975
|
+
let tgtVal;
|
|
976
|
+
if (resolvedTgt.kind === "list") {
|
|
977
|
+
const items = resolvedTgt.items.map((it) => text(resolve(it)).replaceAll(".", "_"));
|
|
978
|
+
tgtVal = items.length === 1 ? items[0] : items;
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
tgtVal = text(resolvedTgt).replaceAll(".", "_");
|
|
982
|
+
}
|
|
983
|
+
f.target = tgtVal;
|
|
984
|
+
if (targetFamTuple) {
|
|
985
|
+
checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else if (targetFamTuple) {
|
|
989
|
+
const matched = resolveFamilyInstruments(targetFamTuple, id, famNode);
|
|
990
|
+
f.target = matched.length === 1 ? matched[0] : matched;
|
|
991
|
+
}
|
|
992
|
+
f.targetKind =
|
|
993
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
994
|
+
? "object"
|
|
995
|
+
: "instrument";
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
999
|
+
if (!target)
|
|
1000
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
1001
|
+
const [root, ...tail] = target.split(".");
|
|
1002
|
+
const resolved = environment.get(root);
|
|
1003
|
+
const resolvedTargets = resolved?.kind === "list"
|
|
1004
|
+
? resolved.items
|
|
1005
|
+
: resolved
|
|
1006
|
+
? [resolved]
|
|
1007
|
+
: [];
|
|
1008
|
+
const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
|
|
1009
|
+
f.target =
|
|
1010
|
+
target === "self"
|
|
1011
|
+
? id
|
|
1012
|
+
: targets.length === 1
|
|
1013
|
+
? targets[0]
|
|
1014
|
+
: targets.length
|
|
1015
|
+
? targets
|
|
1016
|
+
: target;
|
|
1017
|
+
f.targetKind =
|
|
1018
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1019
|
+
? "object"
|
|
1020
|
+
: "instrument";
|
|
1021
|
+
}
|
|
502
1022
|
}
|
|
503
1023
|
else if (type === "account") {
|
|
504
1024
|
if (t.kind === "call") {
|
|
@@ -508,8 +1028,10 @@ export function compile(source, options = {}) {
|
|
|
508
1028
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
509
1029
|
if (t.args[2]) {
|
|
510
1030
|
const mode = text(t.args[2]);
|
|
511
|
-
if (
|
|
512
|
-
|
|
1031
|
+
if (mode === "external")
|
|
1032
|
+
fail(t, "external account mode was removed", "use a reservation and instruction-bound evidence");
|
|
1033
|
+
if (mode === "contra")
|
|
1034
|
+
f.contra = true;
|
|
513
1035
|
else
|
|
514
1036
|
f.key = mode;
|
|
515
1037
|
}
|
|
@@ -620,6 +1142,7 @@ export function compile(source, options = {}) {
|
|
|
620
1142
|
lifecycle.transitions = {};
|
|
621
1143
|
const inst = {
|
|
622
1144
|
id,
|
|
1145
|
+
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
623
1146
|
title: title(id),
|
|
624
1147
|
summary: body.has("summary")
|
|
625
1148
|
? String(data(body.get("summary")))
|
|
@@ -637,21 +1160,213 @@ export function compile(source, options = {}) {
|
|
|
637
1160
|
entries: block.entries.flatMap((entry) => {
|
|
638
1161
|
if (!entry.key.startsWith("when "))
|
|
639
1162
|
return [entry];
|
|
640
|
-
const [, tunable, , choice] = entry.key.split(" ");
|
|
1163
|
+
const [, tunable, relation, choice] = entry.key.split(" ");
|
|
641
1164
|
const binding = environment.get(tunable);
|
|
642
1165
|
if (!binding)
|
|
643
|
-
fail(entry, `unknown branch tunable ${tunable}`, "name an enum tunable declared by this header");
|
|
644
|
-
|
|
645
|
-
|
|
1166
|
+
fail(entry, `unknown branch tunable ${tunable}`, "name an enum or reference tunable declared by this header");
|
|
1167
|
+
let matches;
|
|
1168
|
+
if (relation === "has") {
|
|
1169
|
+
// Inspect declarations, not lowering order: a bound object may
|
|
1170
|
+
// appear after the instrument that asks about its fields.
|
|
1171
|
+
const bound = resolve(binding);
|
|
1172
|
+
const [root, ...children] = text(bound).split(".");
|
|
1173
|
+
const assignment = assignments.get(root);
|
|
1174
|
+
let target = assignment
|
|
1175
|
+
? templates.get(assignment.target)?.body
|
|
1176
|
+
: program.decls
|
|
1177
|
+
.filter((decl) => decl.kind === "instrument")
|
|
1178
|
+
.find((decl) => decl.name === root)?.body;
|
|
1179
|
+
for (const child of children) {
|
|
1180
|
+
const record = target
|
|
1181
|
+
? entries(asBlock(entries(target).get("records"))).get(child)
|
|
1182
|
+
: undefined;
|
|
1183
|
+
target = record ? asBlock(record) : undefined;
|
|
1184
|
+
}
|
|
1185
|
+
if (!target)
|
|
1186
|
+
fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
|
|
1187
|
+
matches = entries(asBlock(entries(target).get("fields"))).has(choice);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
if (!enums.get(tunable)?.includes(choice))
|
|
1191
|
+
fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
|
|
1192
|
+
matches = text(binding) === choice;
|
|
1193
|
+
}
|
|
646
1194
|
const body = asBlock(entry.value);
|
|
647
1195
|
for (const clause of body.entries)
|
|
648
1196
|
if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
|
|
649
1197
|
!clause.key.startsWith("when "))
|
|
650
1198
|
fail(clause, "when permits requirements, calculations, moves and invocations", "keep lifecycle and actor clauses outside the branch");
|
|
651
|
-
return
|
|
1199
|
+
return matches ? selected(body).entries : [];
|
|
652
1200
|
}),
|
|
653
1201
|
});
|
|
654
1202
|
const slots = entries(selected(asBlock(row.value)));
|
|
1203
|
+
let actionSubject;
|
|
1204
|
+
let subjectExpr = slots.get("subject");
|
|
1205
|
+
const boundaryBindings = new Set();
|
|
1206
|
+
const authoredMoves = slots.get("moves");
|
|
1207
|
+
for (const move of authoredMoves?.kind === "list"
|
|
1208
|
+
? authoredMoves.items
|
|
1209
|
+
: authoredMoves
|
|
1210
|
+
? [authoredMoves]
|
|
1211
|
+
: []) {
|
|
1212
|
+
const parts = entries(asBlock(move));
|
|
1213
|
+
const boundary = parts.get("boundary");
|
|
1214
|
+
if (!boundary)
|
|
1215
|
+
continue;
|
|
1216
|
+
if ((parts.has("operation")
|
|
1217
|
+
? String(data(parts.get("operation")))
|
|
1218
|
+
: "internal_transfer.create") !== "internal_transfer.reserve" ||
|
|
1219
|
+
parts.has("shares") ||
|
|
1220
|
+
parts.has("fee"))
|
|
1221
|
+
fail(move, "boundary dispatch requires a reservation", "reserve the exact amount before dispatch");
|
|
1222
|
+
const adapterExpr = entries(asBlock(boundary)).get("adapter");
|
|
1223
|
+
if (!adapterExpr)
|
|
1224
|
+
fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
|
|
1225
|
+
const binding = text(resolve(adapterExpr));
|
|
1226
|
+
const target = options.adapterRegistry &&
|
|
1227
|
+
Object.hasOwn(options.adapterRegistry, binding)
|
|
1228
|
+
? options.adapterRegistry[binding]
|
|
1229
|
+
: undefined;
|
|
1230
|
+
if (!target ||
|
|
1231
|
+
!Object.hasOwn(target.adapter.operationMap, target.operation))
|
|
1232
|
+
fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
|
|
1233
|
+
boundaryBindings.add(binding);
|
|
1234
|
+
}
|
|
1235
|
+
if (boundaryBindings.size) {
|
|
1236
|
+
const block = asBlock(subjectExpr);
|
|
1237
|
+
const declared = new Set(block.entries
|
|
1238
|
+
.filter((entry) => entry.key === "adapter")
|
|
1239
|
+
.flatMap((entry) => entry.value.kind === "list"
|
|
1240
|
+
? entry.value.items.map(text)
|
|
1241
|
+
: [text(entry.value)]));
|
|
1242
|
+
subjectExpr = {
|
|
1243
|
+
...block,
|
|
1244
|
+
entries: [
|
|
1245
|
+
...block.entries,
|
|
1246
|
+
...[...boundaryBindings]
|
|
1247
|
+
.filter((binding) => !declared.has(binding))
|
|
1248
|
+
.map((binding) => ({
|
|
1249
|
+
key: "adapter",
|
|
1250
|
+
value: {
|
|
1251
|
+
kind: "name",
|
|
1252
|
+
value: binding,
|
|
1253
|
+
span: row.span,
|
|
1254
|
+
},
|
|
1255
|
+
span: row.span,
|
|
1256
|
+
})),
|
|
1257
|
+
],
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
if (subjectExpr) {
|
|
1261
|
+
const subjectBlock = asBlock(subjectExpr);
|
|
1262
|
+
const directRequirements = [];
|
|
1263
|
+
const adapterList = [];
|
|
1264
|
+
for (const entry of subjectBlock.entries) {
|
|
1265
|
+
if (entry.key === "adapter") {
|
|
1266
|
+
const bindingNames = entry.value.kind === "list"
|
|
1267
|
+
? entry.value.items.map(text)
|
|
1268
|
+
: [text(entry.value)];
|
|
1269
|
+
for (const bindingName of bindingNames) {
|
|
1270
|
+
const target = options.adapterRegistry?.[bindingName];
|
|
1271
|
+
if (target) {
|
|
1272
|
+
const { adapter, operation } = target;
|
|
1273
|
+
const opBinding = adapter.operationMap[operation];
|
|
1274
|
+
if (opBinding &&
|
|
1275
|
+
opBinding.subjectRequirements !== undefined) {
|
|
1276
|
+
const validatedRequirements = [];
|
|
1277
|
+
for (const req of opBinding.subjectRequirements) {
|
|
1278
|
+
const result = udlObjectFieldSchema.safeParse(req);
|
|
1279
|
+
if (!result.success || result.data.optional) {
|
|
1280
|
+
fail(entry, `adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`, "ensure adapter subject requirements conform to UDL schema");
|
|
1281
|
+
}
|
|
1282
|
+
validatedRequirements.push(result.data);
|
|
1283
|
+
}
|
|
1284
|
+
const declaration = {
|
|
1285
|
+
provider: adapter.provider,
|
|
1286
|
+
capability: adapter.capability,
|
|
1287
|
+
operation,
|
|
1288
|
+
requirements: validatedRequirements,
|
|
1289
|
+
};
|
|
1290
|
+
const digest = sha256(new TextEncoder().encode(canonicalJson(declaration)));
|
|
1291
|
+
const snapshot = {
|
|
1292
|
+
...declaration,
|
|
1293
|
+
declarationDigest: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
1294
|
+
};
|
|
1295
|
+
const adapterRenames = {};
|
|
1296
|
+
if (attachmentInfo?.renames) {
|
|
1297
|
+
for (const req of snapshot.requirements) {
|
|
1298
|
+
if (attachmentInfo.renames.has(req.name)) {
|
|
1299
|
+
adapterRenames[req.name] = attachmentInfo.renames.get(req.name);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
adapterList.push({
|
|
1304
|
+
binding: bindingName,
|
|
1305
|
+
snapshot,
|
|
1306
|
+
...(Object.keys(adapterRenames).length > 0
|
|
1307
|
+
? { renames: adapterRenames }
|
|
1308
|
+
: {}),
|
|
1309
|
+
});
|
|
1310
|
+
for (const reqField of snapshot.requirements) {
|
|
1311
|
+
const objectField = attachmentInfo?.renames.get(reqField.name);
|
|
1312
|
+
const targetName = objectField ?? reqField.name;
|
|
1313
|
+
const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
|
|
1314
|
+
if (existing) {
|
|
1315
|
+
if (!sameObjectField(existing.field, reqField)) {
|
|
1316
|
+
failWithCode(entry, "subject_field_conflict", `conflicting requirement ${reqField.name} in action ${name}`, "rename or unify the requirement");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
directRequirements.push({
|
|
1321
|
+
field: { ...reqField },
|
|
1322
|
+
...(objectField ? { objectField } : {}),
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
adapterList.push({
|
|
1329
|
+
binding: bindingName,
|
|
1330
|
+
snapshot: null,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
adapterList.push({
|
|
1336
|
+
binding: bindingName,
|
|
1337
|
+
snapshot: null,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
const fieldDef = lowerObjectField(entry, resolve);
|
|
1344
|
+
if (fieldDef.optional) {
|
|
1345
|
+
fail(entry, `subject requirement ${entry.key} cannot be optional`, "remove ? from requirement");
|
|
1346
|
+
}
|
|
1347
|
+
const objectField = attachmentInfo?.renames.get(entry.key);
|
|
1348
|
+
const targetName = objectField ?? entry.key;
|
|
1349
|
+
const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
|
|
1350
|
+
if (existing) {
|
|
1351
|
+
if (!sameObjectField(existing.field, fieldDef)) {
|
|
1352
|
+
failWithCode(entry, "subject_field_conflict", `conflicting requirement ${entry.key} in action ${name}`, "rename or unify the requirement");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
directRequirements.push({
|
|
1357
|
+
field: fieldDef,
|
|
1358
|
+
...(objectField ? { objectField } : {}),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (directRequirements.length > 0 || adapterList.length > 0) {
|
|
1364
|
+
actionSubject = {
|
|
1365
|
+
requirements: directRequirements,
|
|
1366
|
+
adapters: adapterList,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
655
1370
|
const a = {
|
|
656
1371
|
summary: slots.has("summary")
|
|
657
1372
|
? String(data(slots.get("summary")))
|
|
@@ -662,7 +1377,14 @@ export function compile(source, options = {}) {
|
|
|
662
1377
|
input: lowerFields(asBlock(slots.get("input"))),
|
|
663
1378
|
requires: [],
|
|
664
1379
|
moves: [],
|
|
1380
|
+
...(actionSubject ? { subject: actionSubject } : {}),
|
|
665
1381
|
};
|
|
1382
|
+
for (const binding of boundaryBindings)
|
|
1383
|
+
if (!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1384
|
+
?.snapshot)
|
|
1385
|
+
fail(row, `boundary adapter ${binding} has no declared requirements`, "declare the adapter subject requirements, including an explicit empty list");
|
|
1386
|
+
currentAction = a;
|
|
1387
|
+
currentActionName = name;
|
|
666
1388
|
if (name !== "create") {
|
|
667
1389
|
const from = slots.get("from");
|
|
668
1390
|
const to = slots.get("to");
|
|
@@ -674,7 +1396,7 @@ export function compile(source, options = {}) {
|
|
|
674
1396
|
};
|
|
675
1397
|
}
|
|
676
1398
|
for (const [key, expr] of slots) {
|
|
677
|
-
if (["from", "to", "input", "summary"].includes(key))
|
|
1399
|
+
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
678
1400
|
continue;
|
|
679
1401
|
if (key === "moves") {
|
|
680
1402
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
@@ -695,6 +1417,17 @@ export function compile(source, options = {}) {
|
|
|
695
1417
|
if (op === "internal_transfer.create" ||
|
|
696
1418
|
op === "internal_transfer.reserve") {
|
|
697
1419
|
const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
|
|
1420
|
+
if (amount) {
|
|
1421
|
+
const resolvedAmount = resolve(amount);
|
|
1422
|
+
if (resolvedAmount.kind === "name" &&
|
|
1423
|
+
resolvedAmount.value.startsWith("subject.")) {
|
|
1424
|
+
const subField = resolvedAmount.value.split(".")[1];
|
|
1425
|
+
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1426
|
+
if (req && req.field.type !== "money") {
|
|
1427
|
+
fail(amount, `move amount subject.${subField} must have type money`, "use a money field");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
698
1431
|
if (parts.has("shares")) {
|
|
699
1432
|
if (!amount ||
|
|
700
1433
|
!from ||
|
|
@@ -715,7 +1448,7 @@ export function compile(source, options = {}) {
|
|
|
715
1448
|
span: rate.span,
|
|
716
1449
|
});
|
|
717
1450
|
if (recipient.kind !== "name" ||
|
|
718
|
-
!
|
|
1451
|
+
!isParty(recipient.value) ||
|
|
719
1452
|
rate.kind !== "percent")
|
|
720
1453
|
fail(rate, "split needs party percentages", "write party_name: 70%");
|
|
721
1454
|
total += Number(literal(rate));
|
|
@@ -770,6 +1503,13 @@ export function compile(source, options = {}) {
|
|
|
770
1503
|
...transfer,
|
|
771
1504
|
operation: op,
|
|
772
1505
|
capture: String(data(parts.get("capture"))),
|
|
1506
|
+
...(parts.has("boundary")
|
|
1507
|
+
? {
|
|
1508
|
+
boundary: {
|
|
1509
|
+
adapter: text(resolve(entries(asBlock(parts.get("boundary"))).get("adapter"))),
|
|
1510
|
+
},
|
|
1511
|
+
}
|
|
1512
|
+
: {}),
|
|
773
1513
|
}
|
|
774
1514
|
: { ...transfer, operation: op });
|
|
775
1515
|
else {
|
|
@@ -888,8 +1628,40 @@ export function compile(source, options = {}) {
|
|
|
888
1628
|
else
|
|
889
1629
|
a[key] = data(expr);
|
|
890
1630
|
}
|
|
1631
|
+
if (attachmentInfo) {
|
|
1632
|
+
if (attachmentInfo.exposed.has(name)) {
|
|
1633
|
+
a.publicAction = attachmentInfo.exposed.get(name);
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
delete a.publicAction;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
891
1639
|
if (automatic(a.actor))
|
|
892
1640
|
delete a.publicAction;
|
|
1641
|
+
const checkSubjectPaths = (obj, span) => {
|
|
1642
|
+
if (typeof obj === "string") {
|
|
1643
|
+
if (obj.startsWith("subject.")) {
|
|
1644
|
+
const subField = obj.split(".")[1];
|
|
1645
|
+
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1646
|
+
if (!req) {
|
|
1647
|
+
failWithCode({ span }, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
else if (Array.isArray(obj)) {
|
|
1652
|
+
for (const item of obj)
|
|
1653
|
+
checkSubjectPaths(item, span);
|
|
1654
|
+
}
|
|
1655
|
+
else if (obj !== null && typeof obj === "object") {
|
|
1656
|
+
for (const val of Object.values(obj))
|
|
1657
|
+
checkSubjectPaths(val, span);
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
checkSubjectPaths(a.requires, row.span);
|
|
1661
|
+
checkSubjectPaths(a.set, row.span);
|
|
1662
|
+
checkSubjectPaths(a.invoke, row.span);
|
|
1663
|
+
currentAction = undefined;
|
|
1664
|
+
currentActionName = undefined;
|
|
893
1665
|
for (const requirement of a.requires ?? []) {
|
|
894
1666
|
if (requirement.kind !== "approval" ||
|
|
895
1667
|
!approvers.has(requirement.party))
|
|
@@ -897,25 +1669,45 @@ export function compile(source, options = {}) {
|
|
|
897
1669
|
const action = requirement.action ?? name;
|
|
898
1670
|
const key = `${id}_${action}_decision`;
|
|
899
1671
|
const previous = implicitDecisions.get(key);
|
|
900
|
-
if (previous &&
|
|
901
|
-
|
|
1672
|
+
if (previous &&
|
|
1673
|
+
(previous.party !== requirement.party ||
|
|
1674
|
+
previous.protectedRequest !==
|
|
1675
|
+
(requirement.protectedRequest ?? "self")))
|
|
1676
|
+
fail({ span: origin }, `action ${action} has conflicting approval parties or protected requests`, "use a separate decision action for each party");
|
|
902
1677
|
implicitDecisions.set(key, {
|
|
903
1678
|
target: id,
|
|
904
1679
|
action,
|
|
905
1680
|
party: requirement.party,
|
|
1681
|
+
protectedRequest: requirement.protectedRequest ?? "self",
|
|
906
1682
|
origin,
|
|
907
1683
|
});
|
|
908
1684
|
}
|
|
1685
|
+
for (const requirement of a.subject?.requirements ?? []) {
|
|
1686
|
+
const entry = asBlock(subjectExpr).entries.find((entry) => entry.key === requirement.field.name) ??
|
|
1687
|
+
asBlock(subjectExpr).entries.find((entry) => entry.key === "adapter");
|
|
1688
|
+
requirementOrigins.set(requirement, {
|
|
1689
|
+
source: declarationSources.get(decl) ?? "program",
|
|
1690
|
+
span: entry?.span ?? row.span,
|
|
1691
|
+
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
909
1694
|
inst.actions[name] = a;
|
|
910
1695
|
inst.actionOrder.push(name);
|
|
911
1696
|
}
|
|
912
|
-
for (const key of ["invariants"])
|
|
1697
|
+
for (const key of ["invariants", "reports", "revisioned"])
|
|
913
1698
|
if (body.has(key))
|
|
914
1699
|
inst[key] = data(body.get(key));
|
|
915
1700
|
origins.push({
|
|
916
1701
|
path: `$.instruments[${document.instruments.length}]`,
|
|
917
1702
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
918
1703
|
});
|
|
1704
|
+
if (familyDeclaration && familyDeclaration.revision !== undefined) {
|
|
1705
|
+
inst.family = {
|
|
1706
|
+
module: familyDeclaration.module,
|
|
1707
|
+
exportPath: familyDeclaration.exportPath,
|
|
1708
|
+
revision: familyDeclaration.revision,
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
919
1711
|
document.instruments.push(inst);
|
|
920
1712
|
for (const [key, definition] of records) {
|
|
921
1713
|
const child = {
|
|
@@ -925,11 +1717,169 @@ export function compile(source, options = {}) {
|
|
|
925
1717
|
body: asBlock(definition),
|
|
926
1718
|
span: origin,
|
|
927
1719
|
};
|
|
1720
|
+
declarationSources.set(child, declarationSources.get(decl) ?? "program");
|
|
1721
|
+
const childFamily = familyDeclaration
|
|
1722
|
+
? {
|
|
1723
|
+
module: familyDeclaration.module,
|
|
1724
|
+
exportPath: `${familyDeclaration.exportPath}.${key}`,
|
|
1725
|
+
...(familyDeclaration.revision !== undefined
|
|
1726
|
+
? { revision: familyDeclaration.revision }
|
|
1727
|
+
: {}),
|
|
1728
|
+
}
|
|
1729
|
+
: undefined;
|
|
928
1730
|
addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
|
|
929
1731
|
...environment,
|
|
930
1732
|
["parent", { kind: "name", value: id, span: origin }],
|
|
931
|
-
]), approvers, enums
|
|
1733
|
+
]), approvers, enums, attachmentInfo
|
|
1734
|
+
? { ...attachmentInfo, exposed: new Map() }
|
|
1735
|
+
: undefined, childFamily);
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
const compileObject = (decl) => {
|
|
1739
|
+
const body = entries(decl.body);
|
|
1740
|
+
for (const key of body.keys()) {
|
|
1741
|
+
if (key !== "fields" &&
|
|
1742
|
+
key !== "columns" &&
|
|
1743
|
+
!key.startsWith("attach ")) {
|
|
1744
|
+
fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
// 1. Lower authored fields
|
|
1748
|
+
const authoredFields = [];
|
|
1749
|
+
const authoredNames = [];
|
|
1750
|
+
const fieldsBlock = asBlock(body.get("fields"));
|
|
1751
|
+
for (const row of fieldsBlock.entries) {
|
|
1752
|
+
if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
|
|
1753
|
+
fail(row, `${row.key} is a reserved object name`, "rename this field");
|
|
1754
|
+
}
|
|
1755
|
+
const fieldDef = lowerObjectField(row);
|
|
1756
|
+
authoredFields.push(fieldDef);
|
|
1757
|
+
authoredNames.push(row.key);
|
|
932
1758
|
}
|
|
1759
|
+
// 2. Process attachments
|
|
1760
|
+
const attachments = [];
|
|
1761
|
+
for (const entry of decl.body.entries) {
|
|
1762
|
+
if (!entry.key.startsWith("attach "))
|
|
1763
|
+
continue;
|
|
1764
|
+
const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
|
|
1765
|
+
if (!match) {
|
|
1766
|
+
fail(entry, "invalid attach syntax", "write attach name = template { ... }");
|
|
1767
|
+
}
|
|
1768
|
+
const attachmentName = match[1];
|
|
1769
|
+
const targetTemplate = match[2];
|
|
1770
|
+
const template = templates.get(targetTemplate);
|
|
1771
|
+
if (!template) {
|
|
1772
|
+
fail(entry, `unknown instrument ${targetTemplate}`, `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`);
|
|
1773
|
+
}
|
|
1774
|
+
const instId = `${decl.name}_${attachmentName}`;
|
|
1775
|
+
const attachmentBlock = asBlock(entry.value);
|
|
1776
|
+
const renames = new Map();
|
|
1777
|
+
const renameEntries = new Map();
|
|
1778
|
+
const exposed = new Map();
|
|
1779
|
+
const tunableEntries = [];
|
|
1780
|
+
for (const row of attachmentBlock.entries) {
|
|
1781
|
+
if (row.key === "rename") {
|
|
1782
|
+
for (const r of asBlock(row.value).entries) {
|
|
1783
|
+
renames.set(r.key, text(r.value));
|
|
1784
|
+
renameEntries.set(r.key, r);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
else if (row.key === "expose") {
|
|
1788
|
+
if (row.value.kind === "call") {
|
|
1789
|
+
const actionName = row.value.name;
|
|
1790
|
+
const publicName = text(row.value.args[0]);
|
|
1791
|
+
exposed.set(actionName, publicName);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
tunableEntries.push(row);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const parties = {};
|
|
1799
|
+
attachments.push({ name: attachmentName, instrument: instId, parties });
|
|
1800
|
+
const tunableBlock = {
|
|
1801
|
+
kind: "block",
|
|
1802
|
+
entries: tunableEntries,
|
|
1803
|
+
span: entry.value.span,
|
|
1804
|
+
};
|
|
1805
|
+
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
1806
|
+
addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Set(), new Map(), {
|
|
1807
|
+
subjectKindId: decl.name,
|
|
1808
|
+
attachmentName,
|
|
1809
|
+
renames,
|
|
1810
|
+
exposed,
|
|
1811
|
+
parties,
|
|
1812
|
+
}, templateFamily ? { ...templateFamily } : undefined);
|
|
1813
|
+
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
1814
|
+
if (attachedInst?.actions.create) {
|
|
1815
|
+
const owned = new Set(attachedInst.calculate.map((node) => node.target));
|
|
1816
|
+
for (const action of Object.values(attachedInst.actions)) {
|
|
1817
|
+
for (const node of action.calculate ?? [])
|
|
1818
|
+
owned.add(node.target);
|
|
1819
|
+
for (const move of action.moves)
|
|
1820
|
+
if ("capture" in move && move.capture)
|
|
1821
|
+
owned.add(move.capture);
|
|
1822
|
+
}
|
|
1823
|
+
const create = attachedInst.actions.create;
|
|
1824
|
+
for (const field of attachedInst.fields) {
|
|
1825
|
+
if (field.type === "account" ||
|
|
1826
|
+
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
1827
|
+
(field.type === "list" &&
|
|
1828
|
+
field.item === "ref" &&
|
|
1829
|
+
field.targetKind === "instrument") ||
|
|
1830
|
+
field.optional ||
|
|
1831
|
+
"value" in field ||
|
|
1832
|
+
owned.has(field.name))
|
|
1833
|
+
continue;
|
|
1834
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
1835
|
+
const existing = create.subject.requirements.find((item) => item.field.name === field.name);
|
|
1836
|
+
if (existing) {
|
|
1837
|
+
if (canonicalJson(existing.field) !== canonicalJson(field))
|
|
1838
|
+
failWithCode(entry, "subject_field_conflict", `${instId}.create.subject.${field.name} conflicts with its instrument field`, "use the instrument field's type and constraints");
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
const objectField = renames.get(field.name);
|
|
1842
|
+
const requirement = {
|
|
1843
|
+
field,
|
|
1844
|
+
...(objectField ? { objectField } : {}),
|
|
1845
|
+
};
|
|
1846
|
+
create.subject.requirements.push(requirement);
|
|
1847
|
+
requirementOrigins.set(requirement, {
|
|
1848
|
+
source: declarationSources.get(template) ?? "program",
|
|
1849
|
+
span: entry.span,
|
|
1850
|
+
message: `${instId}.create.fields.${field.name}`,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
for (const [oldName] of renames) {
|
|
1855
|
+
const found = attachedInst &&
|
|
1856
|
+
Object.values(attachedInst.actions).some((action) => action.subject?.requirements.some((requirement) => requirement.field.name === oldName));
|
|
1857
|
+
if (!found) {
|
|
1858
|
+
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
1859
|
+
failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, "rename a declared subject requirement");
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
// 4. Validate columns
|
|
1864
|
+
const columnsExpr = body.get("columns");
|
|
1865
|
+
let columns = [];
|
|
1866
|
+
if (columnsExpr) {
|
|
1867
|
+
if (columnsExpr.kind !== "list") {
|
|
1868
|
+
fail(columnsExpr, "columns needs a list of field names", "write columns: [name, ...]");
|
|
1869
|
+
}
|
|
1870
|
+
columns = columnsExpr.items.map(text);
|
|
1871
|
+
if (columns.length > 8) {
|
|
1872
|
+
fail(columnsExpr, "at most 8 columns allowed", "choose up to 8 columns");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
document.objects.push({
|
|
1876
|
+
id: decl.name,
|
|
1877
|
+
title: decl.title,
|
|
1878
|
+
authoredFields: authoredNames,
|
|
1879
|
+
attachments,
|
|
1880
|
+
fields: authoredFields,
|
|
1881
|
+
columns,
|
|
1882
|
+
});
|
|
933
1883
|
};
|
|
934
1884
|
for (const decl of program.decls) {
|
|
935
1885
|
if (decl.kind === "instrument") {
|
|
@@ -937,11 +1887,123 @@ export function compile(source, options = {}) {
|
|
|
937
1887
|
fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
|
|
938
1888
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
939
1889
|
}
|
|
940
|
-
if (decl.kind === "
|
|
941
|
-
const template = templates.get(decl.
|
|
1890
|
+
if (decl.kind === "assignment") {
|
|
1891
|
+
const template = templates.get(decl.target);
|
|
942
1892
|
if (!template)
|
|
943
|
-
fail(decl, `unknown
|
|
944
|
-
|
|
1893
|
+
fail(decl, `unknown instrument ${decl.target}`, `add use ${decl.target.split(".")[0]} and choose a declared instrument`);
|
|
1894
|
+
const templateFamily = resolveFamily(decl.target, decl, false);
|
|
1895
|
+
addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Set(), new Map(), undefined, templateFamily ? { ...templateFamily } : undefined);
|
|
1896
|
+
}
|
|
1897
|
+
if (decl.kind === "object") {
|
|
1898
|
+
compileObject(decl);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
// Propagate mandatory invoked action requirements
|
|
1902
|
+
let changedInvocations = true;
|
|
1903
|
+
let invocationIterations = 0;
|
|
1904
|
+
while (changedInvocations && invocationIterations < 32) {
|
|
1905
|
+
changedInvocations = false;
|
|
1906
|
+
invocationIterations++;
|
|
1907
|
+
for (const inst of document.instruments) {
|
|
1908
|
+
for (const action of Object.values(inst.actions)) {
|
|
1909
|
+
for (const call of action.invoke ?? []) {
|
|
1910
|
+
if (call.guard)
|
|
1911
|
+
continue;
|
|
1912
|
+
let targetIds = [];
|
|
1913
|
+
if ("instrument" in call) {
|
|
1914
|
+
targetIds = [call.instrument];
|
|
1915
|
+
}
|
|
1916
|
+
else if ("selection" in call) {
|
|
1917
|
+
targetIds = Array.isArray(call.selection.instrument)
|
|
1918
|
+
? call.selection.instrument
|
|
1919
|
+
: [call.selection.instrument];
|
|
1920
|
+
}
|
|
1921
|
+
else if ("reference" in call) {
|
|
1922
|
+
const refField = resolveField(document, inst, call.reference, action.input, action);
|
|
1923
|
+
if (refField?.type === "ref" &&
|
|
1924
|
+
refField.targetKind === "instrument") {
|
|
1925
|
+
targetIds = Array.isArray(refField.target)
|
|
1926
|
+
? refField.target
|
|
1927
|
+
: [refField.target];
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
for (const targetId of targetIds) {
|
|
1931
|
+
const targetInst = document.instruments.find((i) => i.id === targetId);
|
|
1932
|
+
if (!targetInst)
|
|
1933
|
+
continue;
|
|
1934
|
+
if (inst.subject &&
|
|
1935
|
+
targetInst.subject &&
|
|
1936
|
+
inst.subject === targetInst.subject) {
|
|
1937
|
+
const targetAction = targetInst.actions[call.action];
|
|
1938
|
+
if (!targetAction?.subject)
|
|
1939
|
+
continue;
|
|
1940
|
+
if (!action.subject) {
|
|
1941
|
+
action.subject = { requirements: [], adapters: [] };
|
|
1942
|
+
}
|
|
1943
|
+
for (const adapter of targetAction.subject.adapters) {
|
|
1944
|
+
if (!action.subject.adapters.some((existing) => existing.binding === adapter.binding)) {
|
|
1945
|
+
action.subject.adapters.push(adapter);
|
|
1946
|
+
changedInvocations = true;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
for (const targetReq of targetAction.subject.requirements) {
|
|
1950
|
+
const targetObjFieldName = targetReq.objectField ?? targetReq.field.name;
|
|
1951
|
+
const existing = action.subject.requirements.find((cr) => (cr.objectField ?? cr.field.name) === targetObjFieldName);
|
|
1952
|
+
if (!existing) {
|
|
1953
|
+
const inherited = {
|
|
1954
|
+
field: { ...targetReq.field, name: targetObjFieldName },
|
|
1955
|
+
};
|
|
1956
|
+
action.subject.requirements.push(inherited);
|
|
1957
|
+
requirementOrigins.set(inherited, requirementOrigins.get(targetReq));
|
|
1958
|
+
changedInvocations = true;
|
|
1959
|
+
}
|
|
1960
|
+
else if (!sameObjectField(existing.field, targetReq.field)) {
|
|
1961
|
+
const first = requirementOrigins.get(existing);
|
|
1962
|
+
const second = requirementOrigins.get(targetReq);
|
|
1963
|
+
throw new CompileFailure({
|
|
1964
|
+
code: "subject_field_conflict",
|
|
1965
|
+
message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
|
|
1966
|
+
fix: "ensure compatible requirement definitions across invoked actions",
|
|
1967
|
+
span: first.span,
|
|
1968
|
+
source: first.source,
|
|
1969
|
+
related: [first, second],
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
for (const kind of document.objects) {
|
|
1980
|
+
const origins = new Map(kind.fields.map((field) => [
|
|
1981
|
+
field.name,
|
|
1982
|
+
`authored field ${kind.id}.${field.name}`,
|
|
1983
|
+
]));
|
|
1984
|
+
for (const instrument of document.instruments.filter((item) => item.subject === kind.id)) {
|
|
1985
|
+
for (const name of instrument.actionOrder) {
|
|
1986
|
+
for (const requirement of instrument.actions[name].subject
|
|
1987
|
+
?.requirements ?? []) {
|
|
1988
|
+
const field = {
|
|
1989
|
+
...requirement.field,
|
|
1990
|
+
name: requirement.objectField ?? requirement.field.name,
|
|
1991
|
+
};
|
|
1992
|
+
const previous = kind.fields.find((item) => item.name === field.name);
|
|
1993
|
+
const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
|
|
1994
|
+
if (previous && !sameObjectField(previous, field)) {
|
|
1995
|
+
failWithCode(objects.get(kind.id), "subject_field_conflict", `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`, "rename fields with different meanings");
|
|
1996
|
+
}
|
|
1997
|
+
if (!previous) {
|
|
1998
|
+
kind.fields.push(field);
|
|
1999
|
+
origins.set(field.name, origin);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
for (const column of kind.columns) {
|
|
2005
|
+
if (!kind.fields.some((field) => field.name === column))
|
|
2006
|
+
failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
|
|
945
2007
|
}
|
|
946
2008
|
}
|
|
947
2009
|
const eliminatedStates = new Map();
|
|
@@ -974,7 +2036,8 @@ export function compile(source, options = {}) {
|
|
|
974
2036
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
975
2037
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
976
2038
|
if (edge.from.some((state) => reachable.has(state)))
|
|
977
|
-
|
|
2039
|
+
if (edge.to !== "preserve")
|
|
2040
|
+
reachable.add(edge.to);
|
|
978
2041
|
for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
|
|
979
2042
|
edge.from = edge.from.filter((state) => reachable.has(state));
|
|
980
2043
|
if (!edge.from.length) {
|
|
@@ -1013,13 +2076,36 @@ export function compile(source, options = {}) {
|
|
|
1013
2076
|
continue;
|
|
1014
2077
|
const existing = document.instruments.some((inst) => Object.values(inst.actions).some((action) => action.approval?.action === decision.action &&
|
|
1015
2078
|
action.approval.party === decision.party &&
|
|
1016
|
-
inst.fields.some((field) =>
|
|
1017
|
-
field.
|
|
1018
|
-
|
|
2079
|
+
inst.fields.some((field) => {
|
|
2080
|
+
if (field.type !== "ref" ||
|
|
2081
|
+
field.target !== decision.target ||
|
|
2082
|
+
`self.${field.name}` !== action.approval?.target)
|
|
2083
|
+
return false;
|
|
2084
|
+
const [root, name, ...tail] = decision.protectedRequest.split(".");
|
|
2085
|
+
const input = name && action.approval.input[name];
|
|
2086
|
+
const request = root === "self"
|
|
2087
|
+
? [action.approval.target, name, ...tail]
|
|
2088
|
+
.filter(Boolean)
|
|
2089
|
+
.join(".")
|
|
2090
|
+
: root === "input" && name
|
|
2091
|
+
? input && "field" in input
|
|
2092
|
+
? [input.field, ...tail].join(".")
|
|
2093
|
+
: materialApprovals.has(action)
|
|
2094
|
+
? [`self.material_${name}`, ...tail].join(".")
|
|
2095
|
+
: undefined
|
|
2096
|
+
: undefined;
|
|
2097
|
+
return (request !== undefined &&
|
|
2098
|
+
action.approval.protectedRequest === request);
|
|
2099
|
+
})));
|
|
1019
2100
|
if (existing)
|
|
1020
2101
|
continue;
|
|
1021
2102
|
if (document.instruments.some((inst) => inst.id === id))
|
|
1022
2103
|
fail({ span: decision.origin }, `implicit decision name ${id} is already used`, "rename the conflicting object");
|
|
2104
|
+
const protectedRequest = decision.protectedRequest === "self"
|
|
2105
|
+
? "self.target"
|
|
2106
|
+
: decision.protectedRequest.startsWith("self.")
|
|
2107
|
+
? `self.target.${decision.protectedRequest.slice(5)}`
|
|
2108
|
+
: fail({ span: decision.origin }, "implicit approval needs a stored protected request", "declare an explicit decision for an input-based protected request");
|
|
1023
2109
|
addInstrument(template, id, {
|
|
1024
2110
|
kind: "block",
|
|
1025
2111
|
span: decision.origin,
|
|
@@ -1027,16 +2113,27 @@ export function compile(source, options = {}) {
|
|
|
1027
2113
|
for: decision.target,
|
|
1028
2114
|
approved_by: decision.party,
|
|
1029
2115
|
action: decision.action,
|
|
2116
|
+
protected_request: protectedRequest,
|
|
1030
2117
|
}).map(([key, value]) => ({
|
|
1031
2118
|
key,
|
|
1032
2119
|
value: {
|
|
1033
|
-
kind: key === "action"
|
|
2120
|
+
kind: key === "action" || key === "protected_request"
|
|
2121
|
+
? "text"
|
|
2122
|
+
: "name",
|
|
1034
2123
|
value,
|
|
1035
2124
|
span: decision.origin,
|
|
1036
2125
|
},
|
|
1037
2126
|
span: decision.origin,
|
|
1038
2127
|
})),
|
|
1039
|
-
}, decision.origin)
|
|
2128
|
+
}, decision.origin, new Map(), new Set(), new Map(), document.instruments.find((instrument) => instrument.id === decision.target)?.subject
|
|
2129
|
+
? {
|
|
2130
|
+
subjectKindId: document.instruments.find((instrument) => instrument.id === decision.target).subject,
|
|
2131
|
+
attachmentName: id,
|
|
2132
|
+
parties: {},
|
|
2133
|
+
renames: new Map(),
|
|
2134
|
+
exposed: new Map(),
|
|
2135
|
+
}
|
|
2136
|
+
: undefined);
|
|
1040
2137
|
}
|
|
1041
2138
|
}
|
|
1042
2139
|
// Material approval fields are inferred from the selected action's typed input.
|
|
@@ -1099,7 +2196,7 @@ export function compile(source, options = {}) {
|
|
|
1099
2196
|
.reverse()
|
|
1100
2197
|
.find((o) => i.path.startsWith(o.path));
|
|
1101
2198
|
return diagnostic({
|
|
1102
|
-
code: "HSX1601",
|
|
2199
|
+
code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
|
|
1103
2200
|
message: `${i.path}: ${i.message}`,
|
|
1104
2201
|
fix: i.fix,
|
|
1105
2202
|
span: origin?.span ?? program.span,
|