@hyperscale0/hsx 3.3.0 → 4.0.1
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 +1171 -101
- package/dist/src/compile.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 +2 -3
- 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 +87 -3
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +8 -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 +45 -27
- package/docs/headers.md +43 -44
- package/examples/cost-table.json +8 -324
- package/examples/library.hsx +12 -64
- package/package.json +7 -5
- package/src/ast.ts +11 -1
- package/src/cli.ts +1 -1
- package/src/compile.ts +1607 -118
- package/src/headers.ts +2 -3
- package/src/index.ts +12 -1
- package/src/lex.ts +5 -0
- package/src/parse.ts +84 -3
- package/src/std-bundle.ts +8 -9
- package/src/version.ts +2 -2
- package/std/approvals.hsx +3 -3
- package/std/escrow.hsx +16 -14
- package/std/financing.hsx +59 -17
- package/std/insurance.hsx +4 -5
- package/std/lending.hsx +7 -8
- package/std/marketplace.hsx +2 -5
- package/std/money.hsx +15 -49
- package/std/travel.hsx +5 -6
- package/std/vehicles.hsx +0 -34
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,179 @@ 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 collectChildExportPaths = (parentDecl, suffix) => {
|
|
463
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
464
|
+
const matches = [];
|
|
465
|
+
for (const [recName] of recs) {
|
|
466
|
+
if (suffix === recName)
|
|
467
|
+
matches.push(recName);
|
|
468
|
+
}
|
|
469
|
+
for (const [recName, recBlock] of recs) {
|
|
470
|
+
if (suffix.startsWith(`${recName}_`)) {
|
|
471
|
+
const nested = collectChildExportPaths({
|
|
472
|
+
kind: "instrument",
|
|
473
|
+
name: recName,
|
|
474
|
+
parameters: [],
|
|
475
|
+
body: asBlock(recBlock),
|
|
476
|
+
span: parentDecl.span,
|
|
477
|
+
}, suffix.slice(recName.length + 1));
|
|
478
|
+
for (const rest of nested) {
|
|
479
|
+
matches.push(`${recName}.${rest}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return matches;
|
|
484
|
+
};
|
|
485
|
+
const resolveChildExportPath = (parentDecl, suffix) => {
|
|
486
|
+
const matches = collectChildExportPaths(parentDecl, suffix);
|
|
487
|
+
if (matches.length > 1) {
|
|
488
|
+
failWithCode(parentDecl, "HSX1001", `ambiguous child export path suffix '${suffix}': multiple candidates (${matches.join(", ")})`, "rename conflicting records to remove duplicate export path suffixes");
|
|
489
|
+
}
|
|
490
|
+
return matches[0];
|
|
491
|
+
};
|
|
492
|
+
const getInstrumentFamily = (targetId) => {
|
|
493
|
+
const existing = document.instruments.find((i) => i.id === targetId);
|
|
494
|
+
if (existing?.family)
|
|
495
|
+
return existing.family;
|
|
496
|
+
if (targetId.startsWith(`${id}_`) && familyDeclaration) {
|
|
497
|
+
const sub = resolveChildExportPath(decl, targetId.slice(id.length + 1));
|
|
498
|
+
if (sub) {
|
|
499
|
+
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
500
|
+
try {
|
|
501
|
+
return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
|
|
502
|
+
}
|
|
503
|
+
catch { }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const [asgnName, asgn] of assignments) {
|
|
507
|
+
if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
|
|
508
|
+
const tmpl = templates.get(asgn.target);
|
|
509
|
+
if (!tmpl)
|
|
510
|
+
continue;
|
|
511
|
+
const mod = declarationSources.get(tmpl);
|
|
512
|
+
if (!mod || mod === "program")
|
|
513
|
+
continue;
|
|
514
|
+
if (targetId === asgnName) {
|
|
515
|
+
try {
|
|
516
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
517
|
+
}
|
|
518
|
+
catch { }
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
const sub = resolveChildExportPath(tmpl, targetId.slice(asgnName.length + 1));
|
|
522
|
+
if (sub) {
|
|
523
|
+
const fullTarget = `${asgn.target}.${sub}`;
|
|
524
|
+
try {
|
|
525
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
526
|
+
}
|
|
527
|
+
catch { }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
533
|
+
};
|
|
534
|
+
const checkTargetFamily = (targetIds, expectedFamily, expr) => {
|
|
535
|
+
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
536
|
+
for (const tid of ids) {
|
|
537
|
+
const fam = getInstrumentFamily(tid);
|
|
538
|
+
if (!fam ||
|
|
539
|
+
fam.module !== expectedFamily.module ||
|
|
540
|
+
fam.exportPath !== expectedFamily.exportPath ||
|
|
541
|
+
fam.revision !== expectedFamily.revision) {
|
|
542
|
+
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");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
201
546
|
const enums = new Map(inheritedEnums);
|
|
202
547
|
for (const parameter of decl.parameters) {
|
|
203
548
|
const type = parameter.value.kind === "default"
|
|
@@ -207,23 +552,44 @@ export function compile(source, options = {}) {
|
|
|
207
552
|
enums.set(parameter.key, type.args.map(text));
|
|
208
553
|
}
|
|
209
554
|
const approvers = new Set(inheritedApprovers);
|
|
210
|
-
const supplied =
|
|
555
|
+
const supplied = new Map();
|
|
556
|
+
for (const entry of arguments_.entries) {
|
|
557
|
+
if (supplied.has(entry.key))
|
|
558
|
+
fail(entry, `duplicate tunable ${entry.key}`, "supply each parameter once");
|
|
559
|
+
supplied.set(entry.key, entry.value);
|
|
560
|
+
}
|
|
211
561
|
const environment = new Map(inherited);
|
|
212
562
|
for (const param of decl.parameters) {
|
|
213
563
|
const type = param.value.kind === "default" ? param.value.type : param.value;
|
|
214
564
|
const fallback = param.value.kind === "default" ? param.value.value : undefined;
|
|
215
|
-
const
|
|
565
|
+
const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
|
|
566
|
+
const partyParameter = attachmentInfo && (typeName === "party" || typeName === "approval");
|
|
567
|
+
const byName = partyParameter &&
|
|
568
|
+
(subjectPartyRoles.includes(param.key) ||
|
|
569
|
+
document.parties[param.key])
|
|
570
|
+
? { kind: "name", value: param.key, span: origin }
|
|
571
|
+
: undefined;
|
|
572
|
+
const actual = supplied.get(param.key) ??
|
|
573
|
+
byName ??
|
|
574
|
+
(fallback && {
|
|
575
|
+
...fallback,
|
|
576
|
+
source: declarationSources.get(decl) ?? "program",
|
|
577
|
+
});
|
|
216
578
|
if (!actual) {
|
|
217
579
|
if (type.kind === "type" && type.optional)
|
|
218
580
|
continue;
|
|
219
|
-
|
|
581
|
+
failWithCode({ span: origin }, partyParameter ? "subject_party_unbound" : "HSX1001", `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
|
|
220
582
|
}
|
|
221
583
|
environment.set(param.key, actual);
|
|
222
584
|
}
|
|
223
585
|
for (const key of supplied.keys())
|
|
224
586
|
if (!decl.parameters.some((p) => p.key === key))
|
|
225
587
|
fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
|
|
226
|
-
const
|
|
588
|
+
const isParty = (name) => !!document.parties[name] ||
|
|
589
|
+
(!!attachmentInfo &&
|
|
590
|
+
subjectPartyRoles.includes(name));
|
|
591
|
+
const resolvedParties = new Set();
|
|
592
|
+
const resolve = (expr, seen = new Set(), partyBinding = false) => {
|
|
227
593
|
if (expr.kind === "call" &&
|
|
228
594
|
["object", "all", "party"].includes(expr.name)) {
|
|
229
595
|
if (expr.args.length !== 1)
|
|
@@ -231,15 +597,19 @@ export function compile(source, options = {}) {
|
|
|
231
597
|
const type = text(expr.args[0]);
|
|
232
598
|
const matches = expr.name === "party"
|
|
233
599
|
? Object.entries(document.parties)
|
|
234
|
-
.filter(([, party]) => party.kind === type)
|
|
600
|
+
.filter(([name, party]) => party.kind === type && (!partyBinding || names.has(name)))
|
|
235
601
|
.map(([name]) => name)
|
|
236
|
-
: [...
|
|
237
|
-
.filter((
|
|
238
|
-
|
|
239
|
-
.
|
|
240
|
-
|
|
602
|
+
: [...assignments.values()]
|
|
603
|
+
.filter((assignment) => expr.name === "all" ||
|
|
604
|
+
!attachmentSubjects.has(assignment.name) ||
|
|
605
|
+
attachmentSubjects.get(assignment.name) ===
|
|
606
|
+
attachmentInfo?.subjectKindId)
|
|
607
|
+
.filter((assignment) => assignment.target === type ||
|
|
608
|
+
type.startsWith(`${assignment.target}.`))
|
|
609
|
+
.map((assignment) => assignment.name +
|
|
610
|
+
type.slice(assignment.target.length).replaceAll(".", "_"));
|
|
241
611
|
if (expr.name !== "all" && matches.length !== 1)
|
|
242
|
-
|
|
612
|
+
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
613
|
const items = matches.map((value) => ({
|
|
244
614
|
kind: "name",
|
|
245
615
|
value,
|
|
@@ -251,27 +621,45 @@ export function compile(source, options = {}) {
|
|
|
251
621
|
}
|
|
252
622
|
if (expr.kind !== "name")
|
|
253
623
|
return expr;
|
|
624
|
+
const binding = environment.get(expr.value);
|
|
625
|
+
if (attachmentInfo &&
|
|
626
|
+
subjectPartyRoles.includes(expr.value) &&
|
|
627
|
+
(!binding ||
|
|
628
|
+
(binding.kind === "name" && binding.value === expr.value)))
|
|
629
|
+
return expr;
|
|
630
|
+
if (attachmentInfo) {
|
|
631
|
+
const [local, ...tail] = expr.value.split(".");
|
|
632
|
+
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
633
|
+
if (attachmentSubjects.has(target))
|
|
634
|
+
return { ...expr, value: [target, ...tail].join("_") };
|
|
635
|
+
}
|
|
254
636
|
if (expr.value.startsWith("party.")) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
637
|
+
const [, party, ...members] = expr.value.split(".");
|
|
638
|
+
const binding = environment.get(party);
|
|
639
|
+
if (binding?.kind === "name" && isParty(binding.value))
|
|
640
|
+
return {
|
|
641
|
+
...expr,
|
|
642
|
+
value: ["party", binding.value, ...members].join("."),
|
|
643
|
+
};
|
|
258
644
|
}
|
|
259
645
|
const [root, ...tail] = expr.value.split(".");
|
|
260
646
|
const bound = environment.get(root);
|
|
261
647
|
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
262
648
|
return expr;
|
|
263
649
|
if (seen.has(root))
|
|
264
|
-
return
|
|
265
|
-
let resolved =
|
|
650
|
+
return failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
|
|
651
|
+
let resolved = resolvedParties.has(root) ||
|
|
652
|
+
(!partyBinding &&
|
|
653
|
+
(supplied.has(root) || inherited.has(root) || enums.has(root)))
|
|
266
654
|
? bound
|
|
267
|
-
: resolve(bound, new Set([...seen, root]));
|
|
655
|
+
: resolve(bound, new Set([...seen, root]), partyBinding);
|
|
268
656
|
for (const key of tail) {
|
|
269
657
|
if (resolved.kind !== "block")
|
|
270
658
|
return expr;
|
|
271
659
|
const child = entries(resolved).get(key);
|
|
272
660
|
if (!child)
|
|
273
661
|
return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
|
|
274
|
-
resolved = resolve(child, new Set([...seen, root]));
|
|
662
|
+
resolved = resolve(child, new Set([...seen, root]), partyBinding);
|
|
275
663
|
}
|
|
276
664
|
return resolved;
|
|
277
665
|
};
|
|
@@ -281,7 +669,9 @@ export function compile(source, options = {}) {
|
|
|
281
669
|
continue;
|
|
282
670
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
283
671
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
284
|
-
const v = supplied.has(param.key) || type === "enum"
|
|
672
|
+
const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
673
|
+
? actual
|
|
674
|
+
: resolve(actual, new Set(), !!attachmentInfo && (type === "party" || type === "approval"));
|
|
285
675
|
environment.set(param.key, v);
|
|
286
676
|
if (type === "enum" && t.kind === "call") {
|
|
287
677
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -292,8 +682,21 @@ export function compile(source, options = {}) {
|
|
|
292
682
|
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
293
683
|
}
|
|
294
684
|
else if (type === "party" || type === "approval") {
|
|
295
|
-
if (v.kind !== "name" || !
|
|
296
|
-
|
|
685
|
+
if (v.kind !== "name" || !isParty(v.value))
|
|
686
|
+
failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", `${param.key} needs a declared party`, "declare a party and use its name here");
|
|
687
|
+
const party = document.parties[v.value];
|
|
688
|
+
if ((type === "approval" && (party?.kind !== "staff" || !party.role)) ||
|
|
689
|
+
(type === "party" &&
|
|
690
|
+
(party?.kind === "staff" ||
|
|
691
|
+
(attachmentInfo && party?.kind === "person"))))
|
|
692
|
+
failWithCode(actual, "party_kind_mismatch", `${param.key} cannot bind ${v.value}`, type === "approval"
|
|
693
|
+
? "use a declared staff party with a role"
|
|
694
|
+
: "use a subject role or declared business");
|
|
695
|
+
resolvedParties.add(param.key);
|
|
696
|
+
if (attachmentInfo)
|
|
697
|
+
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
|
|
698
|
+
? { role: v.value }
|
|
699
|
+
: { party: v.value };
|
|
297
700
|
if (type === "approval")
|
|
298
701
|
approvers.add(text(v));
|
|
299
702
|
}
|
|
@@ -309,11 +712,14 @@ export function compile(source, options = {}) {
|
|
|
309
712
|
fail(value, "reference needs an object name", "name a declared object");
|
|
310
713
|
const [root, ...tail] = value.value.split(".");
|
|
311
714
|
const obj = objects.get(root);
|
|
715
|
+
const assignment = assignments.get(root);
|
|
716
|
+
const targetType = obj ? obj.name : assignment?.target;
|
|
312
717
|
if ((!obj &&
|
|
718
|
+
!assignment &&
|
|
313
719
|
!document.instruments.some((inst) => inst.id === value.value)) ||
|
|
314
720
|
(t.kind === "type" &&
|
|
315
721
|
t.target &&
|
|
316
|
-
[
|
|
722
|
+
[targetType, ...tail].join(".") !== t.target))
|
|
317
723
|
fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
|
|
318
724
|
if (seen.has(value.value))
|
|
319
725
|
fail(value, "duplicate reference", "list each object once");
|
|
@@ -373,6 +779,7 @@ export function compile(source, options = {}) {
|
|
|
373
779
|
}
|
|
374
780
|
for (const key of body.keys())
|
|
375
781
|
if (![
|
|
782
|
+
"familyRevision",
|
|
376
783
|
"fields",
|
|
377
784
|
"lifecycle",
|
|
378
785
|
"records",
|
|
@@ -384,6 +791,18 @@ export function compile(source, options = {}) {
|
|
|
384
791
|
].includes(key) &&
|
|
385
792
|
!key.startsWith("action "))
|
|
386
793
|
fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
|
|
794
|
+
if (body.has("familyRevision")) {
|
|
795
|
+
const revExpr = body.get("familyRevision");
|
|
796
|
+
const revVal = literal(revExpr);
|
|
797
|
+
if (typeof revVal !== "number" ||
|
|
798
|
+
!Number.isInteger(revVal) ||
|
|
799
|
+
revVal <= 0) {
|
|
800
|
+
fail(revExpr, "familyRevision must be a positive integer", "use a positive integer revision");
|
|
801
|
+
}
|
|
802
|
+
if (familyDeclaration) {
|
|
803
|
+
familyDeclaration.revision = revVal;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
387
806
|
const records = entries(asBlock(body.get("records")));
|
|
388
807
|
if (!inherited.size)
|
|
389
808
|
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
@@ -396,12 +815,54 @@ export function compile(source, options = {}) {
|
|
|
396
815
|
});
|
|
397
816
|
const fields = [];
|
|
398
817
|
const calculations = [];
|
|
818
|
+
let currentAction;
|
|
819
|
+
let currentActionName;
|
|
399
820
|
const path = (expr) => {
|
|
400
821
|
const value = resolve(expr);
|
|
401
822
|
const name = text(value);
|
|
402
|
-
if (
|
|
823
|
+
if (isParty(name))
|
|
403
824
|
return `party.${name}`;
|
|
404
|
-
|
|
825
|
+
if (name.startsWith("subject.")) {
|
|
826
|
+
const subField = name.split(".")[1];
|
|
827
|
+
if (currentAction) {
|
|
828
|
+
const req = currentAction.subject?.requirements.find((r) => r.field.name === subField);
|
|
829
|
+
if (!req) {
|
|
830
|
+
failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${currentActionName}`, `declare ${subField} in subject { ... }`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
const declaredInAction = decl.body.entries.some((e) => {
|
|
835
|
+
if (!e.key.startsWith("action "))
|
|
836
|
+
return false;
|
|
837
|
+
const subBlock = entries(asBlock(e.value)).get("subject");
|
|
838
|
+
if (!subBlock)
|
|
839
|
+
return false;
|
|
840
|
+
return asBlock(subBlock).entries.some((se) => {
|
|
841
|
+
if (se.key === subField)
|
|
842
|
+
return true;
|
|
843
|
+
if (se.key === "adapter") {
|
|
844
|
+
const names = se.value.kind === "list"
|
|
845
|
+
? se.value.items.map(text)
|
|
846
|
+
: [text(se.value)];
|
|
847
|
+
return names.some((n) => {
|
|
848
|
+
const reg = options.adapterRegistry?.[n];
|
|
849
|
+
if (!reg)
|
|
850
|
+
return false;
|
|
851
|
+
const op = reg.adapter.operationMap[reg.operation];
|
|
852
|
+
return op?.subjectRequirements?.some((sr) => sr.name === subField);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return false;
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
if (!declaredInAction) {
|
|
859
|
+
failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in instrument ${id}`, `declare ${subField} in an action subject { ... }`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return /^(self|input|party|subject)\./.test(name)
|
|
864
|
+
? name
|
|
865
|
+
: `self.${name}`;
|
|
405
866
|
};
|
|
406
867
|
const val = (expr) => {
|
|
407
868
|
const v = resolve(expr);
|
|
@@ -416,7 +877,44 @@ export function compile(source, options = {}) {
|
|
|
416
877
|
return id;
|
|
417
878
|
const value = resolve(expr);
|
|
418
879
|
if (value.kind === "block") {
|
|
419
|
-
const
|
|
880
|
+
const rawEntries = entries(value);
|
|
881
|
+
if ((rawEntries.has("family") && !rawEntries.has("kind")) ||
|
|
882
|
+
((rawEntries.has("states") ||
|
|
883
|
+
rawEntries.has("reference") ||
|
|
884
|
+
rawEntries.has("anchor")) &&
|
|
885
|
+
rawEntries.has("instrument"))) {
|
|
886
|
+
let famTuple;
|
|
887
|
+
if (rawEntries.has("family")) {
|
|
888
|
+
const famExpr = rawEntries.get("family");
|
|
889
|
+
const famStr = famExpr.kind === "name" ? famExpr.value : text(famExpr);
|
|
890
|
+
famTuple = resolveFamily(famStr, famExpr);
|
|
891
|
+
}
|
|
892
|
+
let instrumentVal;
|
|
893
|
+
if (rawEntries.has("instrument")) {
|
|
894
|
+
instrumentVal = data(rawEntries.get("instrument"));
|
|
895
|
+
if (famTuple) {
|
|
896
|
+
checkTargetFamily(instrumentVal, famTuple, rawEntries.get("instrument"));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else if (famTuple) {
|
|
900
|
+
const matched = resolveFamilyInstruments(famTuple, id, rawEntries.get("family"));
|
|
901
|
+
instrumentVal = matched.length === 1 ? matched[0] : matched;
|
|
902
|
+
}
|
|
903
|
+
const result = {};
|
|
904
|
+
if (famTuple) {
|
|
905
|
+
result.family = famTuple;
|
|
906
|
+
}
|
|
907
|
+
if (instrumentVal !== undefined) {
|
|
908
|
+
result.instrument = instrumentVal;
|
|
909
|
+
}
|
|
910
|
+
for (const [k, v] of rawEntries) {
|
|
911
|
+
if (k === "family" || k === "instrument")
|
|
912
|
+
continue;
|
|
913
|
+
result[k] = data(v);
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
|
|
420
918
|
const selection = result.selection;
|
|
421
919
|
if (Array.isArray(selection?.instrument) &&
|
|
422
920
|
selection.instrument.length === 0) {
|
|
@@ -459,56 +957,77 @@ export function compile(source, options = {}) {
|
|
|
459
957
|
for (const row of block.entries) {
|
|
460
958
|
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
461
959
|
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
name: row.key,
|
|
465
|
-
type,
|
|
466
|
-
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
467
|
-
};
|
|
468
|
-
if (type === "enum" && t.kind === "call")
|
|
469
|
-
f.values = t.args.map(text);
|
|
470
|
-
if (type === "text" && t.kind === "call") {
|
|
471
|
-
if (t.args.length < 2 || t.args.length > 3)
|
|
472
|
-
fail(t, "bounded text needs length bounds and an optional pattern", "write text(1, 80)");
|
|
473
|
-
f.minLength = literal(resolve(t.args[0]));
|
|
474
|
-
f.maxLength = literal(resolve(t.args[1]));
|
|
475
|
-
if (t.args[2])
|
|
476
|
-
f.pattern = literal(resolve(t.args[2]));
|
|
477
|
-
}
|
|
478
|
-
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
479
|
-
if (t.args.length !== 2)
|
|
480
|
-
fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
|
|
481
|
-
f.minimum = literal(resolve(t.args[0]));
|
|
482
|
-
f.maximum = literal(resolve(t.args[1]));
|
|
483
|
-
}
|
|
960
|
+
const f = lowerFieldShape(row, resolve);
|
|
961
|
+
const type = f.type;
|
|
484
962
|
if (type === "list" && t.kind === "call") {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
963
|
+
const item = t.args[0];
|
|
964
|
+
if (item.kind === "type" && item.name === "ref" && item.target) {
|
|
965
|
+
f.target = text(resolve({ kind: "name", value: item.target, span: item.span })).replaceAll(".", "_");
|
|
966
|
+
f.targetKind = objects.has(String(f.target))
|
|
967
|
+
? "object"
|
|
968
|
+
: "instrument";
|
|
969
|
+
}
|
|
491
970
|
}
|
|
492
971
|
if (type === "ref") {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
?
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
972
|
+
if (t.kind === "block") {
|
|
973
|
+
const b = entries(t);
|
|
974
|
+
const famNode = b.get("targetFamily") ?? b.get("family");
|
|
975
|
+
let targetFamTuple;
|
|
976
|
+
if (famNode) {
|
|
977
|
+
const famStr = famNode.kind === "name" ? famNode.value : text(famNode);
|
|
978
|
+
targetFamTuple = resolveFamily(famStr, famNode);
|
|
979
|
+
f.targetFamily = targetFamTuple;
|
|
980
|
+
}
|
|
981
|
+
if (b.has("target") || b.has("instrument")) {
|
|
982
|
+
const tgtExpr = (b.get("target") ?? b.get("instrument"));
|
|
983
|
+
const resolvedTgt = resolve(tgtExpr);
|
|
984
|
+
let tgtVal;
|
|
985
|
+
if (resolvedTgt.kind === "list") {
|
|
986
|
+
const items = resolvedTgt.items.map((it) => text(resolve(it)).replaceAll(".", "_"));
|
|
987
|
+
tgtVal = items.length === 1 ? items[0] : items;
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
tgtVal = text(resolvedTgt).replaceAll(".", "_");
|
|
991
|
+
}
|
|
992
|
+
f.target = tgtVal;
|
|
993
|
+
if (targetFamTuple) {
|
|
994
|
+
checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else if (targetFamTuple) {
|
|
998
|
+
const matched = resolveFamilyInstruments(targetFamTuple, id, famNode);
|
|
999
|
+
f.target = matched.length === 1 ? matched[0] : matched;
|
|
1000
|
+
}
|
|
1001
|
+
f.targetKind =
|
|
1002
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1003
|
+
? "object"
|
|
1004
|
+
: "instrument";
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
1008
|
+
if (!target)
|
|
1009
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
1010
|
+
const [root, ...tail] = target.split(".");
|
|
1011
|
+
const resolved = environment.get(root);
|
|
1012
|
+
const resolvedTargets = resolved?.kind === "list"
|
|
1013
|
+
? resolved.items
|
|
1014
|
+
: resolved
|
|
1015
|
+
? [resolved]
|
|
1016
|
+
: [];
|
|
1017
|
+
const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
|
|
1018
|
+
f.target =
|
|
1019
|
+
target === "self"
|
|
1020
|
+
? id
|
|
1021
|
+
: targets.length === 1
|
|
1022
|
+
? targets[0]
|
|
1023
|
+
: targets.length
|
|
1024
|
+
? targets
|
|
1025
|
+
: target;
|
|
1026
|
+
f.targetKind =
|
|
1027
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1028
|
+
? "object"
|
|
1029
|
+
: "instrument";
|
|
1030
|
+
}
|
|
512
1031
|
}
|
|
513
1032
|
else if (type === "account") {
|
|
514
1033
|
if (t.kind === "call") {
|
|
@@ -518,8 +1037,10 @@ export function compile(source, options = {}) {
|
|
|
518
1037
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
519
1038
|
if (t.args[2]) {
|
|
520
1039
|
const mode = text(t.args[2]);
|
|
521
|
-
if (
|
|
522
|
-
|
|
1040
|
+
if (mode === "external")
|
|
1041
|
+
fail(t, "external account mode was removed", "use a reservation and instruction-bound evidence");
|
|
1042
|
+
if (mode === "contra")
|
|
1043
|
+
f.contra = true;
|
|
523
1044
|
else
|
|
524
1045
|
f.key = mode;
|
|
525
1046
|
}
|
|
@@ -630,6 +1151,7 @@ export function compile(source, options = {}) {
|
|
|
630
1151
|
lifecycle.transitions = {};
|
|
631
1152
|
const inst = {
|
|
632
1153
|
id,
|
|
1154
|
+
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
633
1155
|
title: title(id),
|
|
634
1156
|
summary: body.has("summary")
|
|
635
1157
|
? String(data(body.get("summary")))
|
|
@@ -657,9 +1179,9 @@ export function compile(source, options = {}) {
|
|
|
657
1179
|
// appear after the instrument that asks about its fields.
|
|
658
1180
|
const bound = resolve(binding);
|
|
659
1181
|
const [root, ...children] = text(bound).split(".");
|
|
660
|
-
const
|
|
661
|
-
let target =
|
|
662
|
-
? templates.get(
|
|
1182
|
+
const assignment = assignments.get(root);
|
|
1183
|
+
let target = assignment
|
|
1184
|
+
? templates.get(assignment.target)?.body
|
|
663
1185
|
: program.decls
|
|
664
1186
|
.filter((decl) => decl.kind === "instrument")
|
|
665
1187
|
.find((decl) => decl.name === root)?.body;
|
|
@@ -687,6 +1209,173 @@ export function compile(source, options = {}) {
|
|
|
687
1209
|
}),
|
|
688
1210
|
});
|
|
689
1211
|
const slots = entries(selected(asBlock(row.value)));
|
|
1212
|
+
let actionSubject;
|
|
1213
|
+
let subjectExpr = slots.get("subject");
|
|
1214
|
+
const boundaryBindings = new Set();
|
|
1215
|
+
const authoredMoves = slots.get("moves");
|
|
1216
|
+
for (const move of authoredMoves?.kind === "list"
|
|
1217
|
+
? authoredMoves.items
|
|
1218
|
+
: authoredMoves
|
|
1219
|
+
? [authoredMoves]
|
|
1220
|
+
: []) {
|
|
1221
|
+
const parts = entries(asBlock(move));
|
|
1222
|
+
const boundary = parts.get("boundary");
|
|
1223
|
+
if (!boundary)
|
|
1224
|
+
continue;
|
|
1225
|
+
if ((parts.has("operation")
|
|
1226
|
+
? String(data(parts.get("operation")))
|
|
1227
|
+
: "internal_transfer.create") !== "internal_transfer.reserve" ||
|
|
1228
|
+
parts.has("shares") ||
|
|
1229
|
+
parts.has("fee"))
|
|
1230
|
+
fail(move, "boundary dispatch requires a reservation", "reserve the exact amount before dispatch");
|
|
1231
|
+
const adapterExpr = entries(asBlock(boundary)).get("adapter");
|
|
1232
|
+
if (!adapterExpr)
|
|
1233
|
+
fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
|
|
1234
|
+
const binding = text(resolve(adapterExpr));
|
|
1235
|
+
const target = options.adapterRegistry &&
|
|
1236
|
+
Object.hasOwn(options.adapterRegistry, binding)
|
|
1237
|
+
? options.adapterRegistry[binding]
|
|
1238
|
+
: undefined;
|
|
1239
|
+
if (!target ||
|
|
1240
|
+
!Object.hasOwn(target.adapter.operationMap, target.operation))
|
|
1241
|
+
fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
|
|
1242
|
+
boundaryBindings.add(binding);
|
|
1243
|
+
}
|
|
1244
|
+
if (boundaryBindings.size) {
|
|
1245
|
+
const block = asBlock(subjectExpr);
|
|
1246
|
+
const declared = new Set(block.entries
|
|
1247
|
+
.filter((entry) => entry.key === "adapter")
|
|
1248
|
+
.flatMap((entry) => entry.value.kind === "list"
|
|
1249
|
+
? entry.value.items.map(text)
|
|
1250
|
+
: [text(entry.value)]));
|
|
1251
|
+
subjectExpr = {
|
|
1252
|
+
...block,
|
|
1253
|
+
entries: [
|
|
1254
|
+
...block.entries,
|
|
1255
|
+
...[...boundaryBindings]
|
|
1256
|
+
.filter((binding) => !declared.has(binding))
|
|
1257
|
+
.map((binding) => ({
|
|
1258
|
+
key: "adapter",
|
|
1259
|
+
value: {
|
|
1260
|
+
kind: "name",
|
|
1261
|
+
value: binding,
|
|
1262
|
+
span: row.span,
|
|
1263
|
+
},
|
|
1264
|
+
span: row.span,
|
|
1265
|
+
})),
|
|
1266
|
+
],
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (subjectExpr) {
|
|
1270
|
+
const subjectBlock = asBlock(subjectExpr);
|
|
1271
|
+
const directRequirements = [];
|
|
1272
|
+
const adapterList = [];
|
|
1273
|
+
for (const entry of subjectBlock.entries) {
|
|
1274
|
+
if (entry.key === "adapter") {
|
|
1275
|
+
const bindingNames = entry.value.kind === "list"
|
|
1276
|
+
? entry.value.items.map(text)
|
|
1277
|
+
: [text(entry.value)];
|
|
1278
|
+
for (const bindingName of bindingNames) {
|
|
1279
|
+
const target = options.adapterRegistry?.[bindingName];
|
|
1280
|
+
if (target) {
|
|
1281
|
+
const { adapter, operation } = target;
|
|
1282
|
+
const opBinding = adapter.operationMap[operation];
|
|
1283
|
+
if (opBinding &&
|
|
1284
|
+
opBinding.subjectRequirements !== undefined) {
|
|
1285
|
+
const validatedRequirements = [];
|
|
1286
|
+
for (const req of opBinding.subjectRequirements) {
|
|
1287
|
+
const result = udlObjectFieldSchema.safeParse(req);
|
|
1288
|
+
if (!result.success || result.data.optional) {
|
|
1289
|
+
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");
|
|
1290
|
+
}
|
|
1291
|
+
validatedRequirements.push(result.data);
|
|
1292
|
+
}
|
|
1293
|
+
const declaration = {
|
|
1294
|
+
provider: adapter.provider,
|
|
1295
|
+
capability: adapter.capability,
|
|
1296
|
+
operation,
|
|
1297
|
+
requirements: validatedRequirements,
|
|
1298
|
+
};
|
|
1299
|
+
const digest = sha256(new TextEncoder().encode(canonicalJson(declaration)));
|
|
1300
|
+
const snapshot = {
|
|
1301
|
+
...declaration,
|
|
1302
|
+
declarationDigest: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
1303
|
+
};
|
|
1304
|
+
const adapterRenames = {};
|
|
1305
|
+
if (attachmentInfo?.renames) {
|
|
1306
|
+
for (const req of snapshot.requirements) {
|
|
1307
|
+
if (attachmentInfo.renames.has(req.name)) {
|
|
1308
|
+
adapterRenames[req.name] = attachmentInfo.renames.get(req.name);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
adapterList.push({
|
|
1313
|
+
binding: bindingName,
|
|
1314
|
+
snapshot,
|
|
1315
|
+
...(Object.keys(adapterRenames).length > 0
|
|
1316
|
+
? { renames: adapterRenames }
|
|
1317
|
+
: {}),
|
|
1318
|
+
});
|
|
1319
|
+
for (const reqField of snapshot.requirements) {
|
|
1320
|
+
const objectField = attachmentInfo?.renames.get(reqField.name);
|
|
1321
|
+
const targetName = objectField ?? reqField.name;
|
|
1322
|
+
const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
|
|
1323
|
+
if (existing) {
|
|
1324
|
+
if (!sameObjectField(existing.field, reqField)) {
|
|
1325
|
+
failWithCode(entry, "subject_field_conflict", `conflicting requirement ${reqField.name} in action ${name}`, "rename or unify the requirement");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
directRequirements.push({
|
|
1330
|
+
field: { ...reqField },
|
|
1331
|
+
...(objectField ? { objectField } : {}),
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
adapterList.push({
|
|
1338
|
+
binding: bindingName,
|
|
1339
|
+
snapshot: null,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
adapterList.push({
|
|
1345
|
+
binding: bindingName,
|
|
1346
|
+
snapshot: null,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
const fieldDef = lowerObjectField(entry, resolve);
|
|
1353
|
+
if (fieldDef.optional) {
|
|
1354
|
+
fail(entry, `subject requirement ${entry.key} cannot be optional`, "remove ? from requirement");
|
|
1355
|
+
}
|
|
1356
|
+
const objectField = attachmentInfo?.renames.get(entry.key);
|
|
1357
|
+
const targetName = objectField ?? entry.key;
|
|
1358
|
+
const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
|
|
1359
|
+
if (existing) {
|
|
1360
|
+
if (!sameObjectField(existing.field, fieldDef)) {
|
|
1361
|
+
failWithCode(entry, "subject_field_conflict", `conflicting requirement ${entry.key} in action ${name}`, "rename or unify the requirement");
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
directRequirements.push({
|
|
1366
|
+
field: fieldDef,
|
|
1367
|
+
...(objectField ? { objectField } : {}),
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (directRequirements.length > 0 || adapterList.length > 0) {
|
|
1373
|
+
actionSubject = {
|
|
1374
|
+
requirements: directRequirements,
|
|
1375
|
+
adapters: adapterList,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
690
1379
|
const a = {
|
|
691
1380
|
summary: slots.has("summary")
|
|
692
1381
|
? String(data(slots.get("summary")))
|
|
@@ -697,7 +1386,14 @@ export function compile(source, options = {}) {
|
|
|
697
1386
|
input: lowerFields(asBlock(slots.get("input"))),
|
|
698
1387
|
requires: [],
|
|
699
1388
|
moves: [],
|
|
1389
|
+
...(actionSubject ? { subject: actionSubject } : {}),
|
|
700
1390
|
};
|
|
1391
|
+
for (const binding of boundaryBindings)
|
|
1392
|
+
if (!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1393
|
+
?.snapshot)
|
|
1394
|
+
fail(row, `boundary adapter ${binding} has no declared requirements`, "declare the adapter subject requirements, including an explicit empty list");
|
|
1395
|
+
currentAction = a;
|
|
1396
|
+
currentActionName = name;
|
|
701
1397
|
if (name !== "create") {
|
|
702
1398
|
const from = slots.get("from");
|
|
703
1399
|
const to = slots.get("to");
|
|
@@ -709,7 +1405,7 @@ export function compile(source, options = {}) {
|
|
|
709
1405
|
};
|
|
710
1406
|
}
|
|
711
1407
|
for (const [key, expr] of slots) {
|
|
712
|
-
if (["from", "to", "input", "summary"].includes(key))
|
|
1408
|
+
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
713
1409
|
continue;
|
|
714
1410
|
if (key === "moves") {
|
|
715
1411
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
@@ -730,6 +1426,17 @@ export function compile(source, options = {}) {
|
|
|
730
1426
|
if (op === "internal_transfer.create" ||
|
|
731
1427
|
op === "internal_transfer.reserve") {
|
|
732
1428
|
const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
|
|
1429
|
+
if (amount) {
|
|
1430
|
+
const resolvedAmount = resolve(amount);
|
|
1431
|
+
if (resolvedAmount.kind === "name" &&
|
|
1432
|
+
resolvedAmount.value.startsWith("subject.")) {
|
|
1433
|
+
const subField = resolvedAmount.value.split(".")[1];
|
|
1434
|
+
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1435
|
+
if (req && req.field.type !== "money") {
|
|
1436
|
+
fail(amount, `move amount subject.${subField} must have type money`, "use a money field");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
733
1440
|
if (parts.has("shares")) {
|
|
734
1441
|
if (!amount ||
|
|
735
1442
|
!from ||
|
|
@@ -750,7 +1457,7 @@ export function compile(source, options = {}) {
|
|
|
750
1457
|
span: rate.span,
|
|
751
1458
|
});
|
|
752
1459
|
if (recipient.kind !== "name" ||
|
|
753
|
-
!
|
|
1460
|
+
!isParty(recipient.value) ||
|
|
754
1461
|
rate.kind !== "percent")
|
|
755
1462
|
fail(rate, "split needs party percentages", "write party_name: 70%");
|
|
756
1463
|
total += Number(literal(rate));
|
|
@@ -805,6 +1512,13 @@ export function compile(source, options = {}) {
|
|
|
805
1512
|
...transfer,
|
|
806
1513
|
operation: op,
|
|
807
1514
|
capture: String(data(parts.get("capture"))),
|
|
1515
|
+
...(parts.has("boundary")
|
|
1516
|
+
? {
|
|
1517
|
+
boundary: {
|
|
1518
|
+
adapter: text(resolve(entries(asBlock(parts.get("boundary"))).get("adapter"))),
|
|
1519
|
+
},
|
|
1520
|
+
}
|
|
1521
|
+
: {}),
|
|
808
1522
|
}
|
|
809
1523
|
: { ...transfer, operation: op });
|
|
810
1524
|
else {
|
|
@@ -923,8 +1637,40 @@ export function compile(source, options = {}) {
|
|
|
923
1637
|
else
|
|
924
1638
|
a[key] = data(expr);
|
|
925
1639
|
}
|
|
1640
|
+
if (attachmentInfo) {
|
|
1641
|
+
if (attachmentInfo.exposed.has(name)) {
|
|
1642
|
+
a.publicAction = attachmentInfo.exposed.get(name);
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
delete a.publicAction;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
926
1648
|
if (automatic(a.actor))
|
|
927
1649
|
delete a.publicAction;
|
|
1650
|
+
const checkSubjectPaths = (obj, span) => {
|
|
1651
|
+
if (typeof obj === "string") {
|
|
1652
|
+
if (obj.startsWith("subject.")) {
|
|
1653
|
+
const subField = obj.split(".")[1];
|
|
1654
|
+
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1655
|
+
if (!req) {
|
|
1656
|
+
failWithCode({ span }, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
else if (Array.isArray(obj)) {
|
|
1661
|
+
for (const item of obj)
|
|
1662
|
+
checkSubjectPaths(item, span);
|
|
1663
|
+
}
|
|
1664
|
+
else if (obj !== null && typeof obj === "object") {
|
|
1665
|
+
for (const val of Object.values(obj))
|
|
1666
|
+
checkSubjectPaths(val, span);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
checkSubjectPaths(a.requires, row.span);
|
|
1670
|
+
checkSubjectPaths(a.set, row.span);
|
|
1671
|
+
checkSubjectPaths(a.invoke, row.span);
|
|
1672
|
+
currentAction = undefined;
|
|
1673
|
+
currentActionName = undefined;
|
|
928
1674
|
for (const requirement of a.requires ?? []) {
|
|
929
1675
|
if (requirement.kind !== "approval" ||
|
|
930
1676
|
!approvers.has(requirement.party))
|
|
@@ -932,15 +1678,28 @@ export function compile(source, options = {}) {
|
|
|
932
1678
|
const action = requirement.action ?? name;
|
|
933
1679
|
const key = `${id}_${action}_decision`;
|
|
934
1680
|
const previous = implicitDecisions.get(key);
|
|
935
|
-
if (previous &&
|
|
936
|
-
|
|
1681
|
+
if (previous &&
|
|
1682
|
+
(previous.party !== requirement.party ||
|
|
1683
|
+
previous.protectedRequest !==
|
|
1684
|
+
(requirement.protectedRequest ?? "self")))
|
|
1685
|
+
fail({ span: origin }, `action ${action} has conflicting approval parties or protected requests`, "use a separate decision action for each party");
|
|
937
1686
|
implicitDecisions.set(key, {
|
|
938
1687
|
target: id,
|
|
939
1688
|
action,
|
|
940
1689
|
party: requirement.party,
|
|
1690
|
+
protectedRequest: requirement.protectedRequest ?? "self",
|
|
941
1691
|
origin,
|
|
942
1692
|
});
|
|
943
1693
|
}
|
|
1694
|
+
for (const requirement of a.subject?.requirements ?? []) {
|
|
1695
|
+
const entry = asBlock(subjectExpr).entries.find((entry) => entry.key === requirement.field.name) ??
|
|
1696
|
+
asBlock(subjectExpr).entries.find((entry) => entry.key === "adapter");
|
|
1697
|
+
requirementOrigins.set(requirement, {
|
|
1698
|
+
source: declarationSources.get(decl) ?? "program",
|
|
1699
|
+
span: entry?.span ?? row.span,
|
|
1700
|
+
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
944
1703
|
inst.actions[name] = a;
|
|
945
1704
|
inst.actionOrder.push(name);
|
|
946
1705
|
}
|
|
@@ -951,6 +1710,13 @@ export function compile(source, options = {}) {
|
|
|
951
1710
|
path: `$.instruments[${document.instruments.length}]`,
|
|
952
1711
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
953
1712
|
});
|
|
1713
|
+
if (familyDeclaration && familyDeclaration.revision !== undefined) {
|
|
1714
|
+
inst.family = {
|
|
1715
|
+
module: familyDeclaration.module,
|
|
1716
|
+
exportPath: familyDeclaration.exportPath,
|
|
1717
|
+
revision: familyDeclaration.revision,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
954
1720
|
document.instruments.push(inst);
|
|
955
1721
|
for (const [key, definition] of records) {
|
|
956
1722
|
const child = {
|
|
@@ -960,23 +1726,293 @@ export function compile(source, options = {}) {
|
|
|
960
1726
|
body: asBlock(definition),
|
|
961
1727
|
span: origin,
|
|
962
1728
|
};
|
|
1729
|
+
declarationSources.set(child, declarationSources.get(decl) ?? "program");
|
|
1730
|
+
const childFamily = familyDeclaration
|
|
1731
|
+
? {
|
|
1732
|
+
module: familyDeclaration.module,
|
|
1733
|
+
exportPath: `${familyDeclaration.exportPath}.${key}`,
|
|
1734
|
+
...(familyDeclaration.revision !== undefined
|
|
1735
|
+
? { revision: familyDeclaration.revision }
|
|
1736
|
+
: {}),
|
|
1737
|
+
}
|
|
1738
|
+
: undefined;
|
|
963
1739
|
addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
|
|
964
1740
|
...environment,
|
|
965
1741
|
["parent", { kind: "name", value: id, span: origin }],
|
|
966
|
-
]), approvers, enums
|
|
1742
|
+
]), approvers, enums, attachmentInfo
|
|
1743
|
+
? { ...attachmentInfo, exposed: new Map() }
|
|
1744
|
+
: undefined, childFamily);
|
|
967
1745
|
}
|
|
968
1746
|
};
|
|
1747
|
+
const compileObject = (decl) => {
|
|
1748
|
+
const body = entries(decl.body);
|
|
1749
|
+
for (const key of body.keys()) {
|
|
1750
|
+
if (key !== "fields" &&
|
|
1751
|
+
key !== "columns" &&
|
|
1752
|
+
!key.startsWith("attach ")) {
|
|
1753
|
+
fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
// 1. Lower authored fields
|
|
1757
|
+
const authoredFields = [];
|
|
1758
|
+
const authoredNames = [];
|
|
1759
|
+
const fieldsBlock = asBlock(body.get("fields"));
|
|
1760
|
+
for (const row of fieldsBlock.entries) {
|
|
1761
|
+
if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
|
|
1762
|
+
fail(row, `${row.key} is a reserved object name`, "rename this field");
|
|
1763
|
+
}
|
|
1764
|
+
const fieldDef = lowerObjectField(row);
|
|
1765
|
+
authoredFields.push(fieldDef);
|
|
1766
|
+
authoredNames.push(row.key);
|
|
1767
|
+
}
|
|
1768
|
+
// 2. Process attachments
|
|
1769
|
+
const attachments = [];
|
|
1770
|
+
for (const entry of decl.body.entries) {
|
|
1771
|
+
if (!entry.key.startsWith("attach "))
|
|
1772
|
+
continue;
|
|
1773
|
+
const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
|
|
1774
|
+
if (!match) {
|
|
1775
|
+
fail(entry, "invalid attach syntax", "write attach name = template { ... }");
|
|
1776
|
+
}
|
|
1777
|
+
const attachmentName = match[1];
|
|
1778
|
+
const targetTemplate = match[2];
|
|
1779
|
+
const template = templates.get(targetTemplate);
|
|
1780
|
+
if (!template) {
|
|
1781
|
+
fail(entry, `unknown instrument ${targetTemplate}`, `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`);
|
|
1782
|
+
}
|
|
1783
|
+
const instId = `${decl.name}_${attachmentName}`;
|
|
1784
|
+
const attachmentBlock = asBlock(entry.value);
|
|
1785
|
+
const renames = new Map();
|
|
1786
|
+
const renameEntries = new Map();
|
|
1787
|
+
const exposed = new Map();
|
|
1788
|
+
const tunableEntries = [];
|
|
1789
|
+
for (const row of attachmentBlock.entries) {
|
|
1790
|
+
if (row.key === "rename") {
|
|
1791
|
+
for (const r of asBlock(row.value).entries) {
|
|
1792
|
+
renames.set(r.key, text(r.value));
|
|
1793
|
+
renameEntries.set(r.key, r);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
else if (row.key === "expose") {
|
|
1797
|
+
if (row.value.kind === "call") {
|
|
1798
|
+
const actionName = row.value.name;
|
|
1799
|
+
const publicName = text(row.value.args[0]);
|
|
1800
|
+
exposed.set(actionName, publicName);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
tunableEntries.push(row);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const parties = {};
|
|
1808
|
+
attachments.push({ name: attachmentName, instrument: instId, parties });
|
|
1809
|
+
const tunableBlock = {
|
|
1810
|
+
kind: "block",
|
|
1811
|
+
entries: tunableEntries,
|
|
1812
|
+
span: entry.value.span,
|
|
1813
|
+
};
|
|
1814
|
+
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
1815
|
+
addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Set(), new Map(), {
|
|
1816
|
+
subjectKindId: decl.name,
|
|
1817
|
+
attachmentName,
|
|
1818
|
+
renames,
|
|
1819
|
+
exposed,
|
|
1820
|
+
parties,
|
|
1821
|
+
}, templateFamily ? { ...templateFamily } : undefined);
|
|
1822
|
+
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
1823
|
+
if (attachedInst?.actions.create) {
|
|
1824
|
+
const owned = new Set(attachedInst.calculate.map((node) => node.target));
|
|
1825
|
+
for (const action of Object.values(attachedInst.actions)) {
|
|
1826
|
+
for (const node of action.calculate ?? [])
|
|
1827
|
+
owned.add(node.target);
|
|
1828
|
+
for (const move of action.moves)
|
|
1829
|
+
if ("capture" in move && move.capture)
|
|
1830
|
+
owned.add(move.capture);
|
|
1831
|
+
}
|
|
1832
|
+
const create = attachedInst.actions.create;
|
|
1833
|
+
for (const field of attachedInst.fields) {
|
|
1834
|
+
if (field.type === "account" ||
|
|
1835
|
+
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
1836
|
+
(field.type === "list" &&
|
|
1837
|
+
field.item === "ref" &&
|
|
1838
|
+
field.targetKind === "instrument") ||
|
|
1839
|
+
field.optional ||
|
|
1840
|
+
"value" in field ||
|
|
1841
|
+
owned.has(field.name))
|
|
1842
|
+
continue;
|
|
1843
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
1844
|
+
const existing = create.subject.requirements.find((item) => item.field.name === field.name);
|
|
1845
|
+
if (existing) {
|
|
1846
|
+
if (canonicalJson(existing.field) !== canonicalJson(field))
|
|
1847
|
+
failWithCode(entry, "subject_field_conflict", `${instId}.create.subject.${field.name} conflicts with its instrument field`, "use the instrument field's type and constraints");
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const objectField = renames.get(field.name);
|
|
1851
|
+
const requirement = {
|
|
1852
|
+
field,
|
|
1853
|
+
...(objectField ? { objectField } : {}),
|
|
1854
|
+
};
|
|
1855
|
+
create.subject.requirements.push(requirement);
|
|
1856
|
+
requirementOrigins.set(requirement, {
|
|
1857
|
+
source: declarationSources.get(template) ?? "program",
|
|
1858
|
+
span: entry.span,
|
|
1859
|
+
message: `${instId}.create.fields.${field.name}`,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
for (const [oldName] of renames) {
|
|
1864
|
+
const found = attachedInst &&
|
|
1865
|
+
Object.values(attachedInst.actions).some((action) => action.subject?.requirements.some((requirement) => requirement.field.name === oldName));
|
|
1866
|
+
if (!found) {
|
|
1867
|
+
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
1868
|
+
failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, "rename a declared subject requirement");
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
// 4. Validate columns
|
|
1873
|
+
const columnsExpr = body.get("columns");
|
|
1874
|
+
let columns = [];
|
|
1875
|
+
if (columnsExpr) {
|
|
1876
|
+
if (columnsExpr.kind !== "list") {
|
|
1877
|
+
fail(columnsExpr, "columns needs a list of field names", "write columns: [name, ...]");
|
|
1878
|
+
}
|
|
1879
|
+
columns = columnsExpr.items.map(text);
|
|
1880
|
+
if (columns.length > 8) {
|
|
1881
|
+
fail(columnsExpr, "at most 8 columns allowed", "choose up to 8 columns");
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
document.objects.push({
|
|
1885
|
+
id: decl.name,
|
|
1886
|
+
title: decl.title,
|
|
1887
|
+
authoredFields: authoredNames,
|
|
1888
|
+
attachments,
|
|
1889
|
+
fields: authoredFields,
|
|
1890
|
+
columns,
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
969
1893
|
for (const decl of program.decls) {
|
|
970
1894
|
if (decl.kind === "instrument") {
|
|
971
1895
|
if (decl.parameters.length)
|
|
972
1896
|
fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
|
|
973
1897
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
974
1898
|
}
|
|
975
|
-
if (decl.kind === "
|
|
976
|
-
const template = templates.get(decl.
|
|
1899
|
+
if (decl.kind === "assignment") {
|
|
1900
|
+
const template = templates.get(decl.target);
|
|
977
1901
|
if (!template)
|
|
978
|
-
fail(decl, `unknown
|
|
979
|
-
|
|
1902
|
+
fail(decl, `unknown instrument ${decl.target}`, `add use ${decl.target.split(".")[0]} and choose a declared instrument`);
|
|
1903
|
+
const templateFamily = resolveFamily(decl.target, decl, false);
|
|
1904
|
+
addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Set(), new Map(), undefined, templateFamily ? { ...templateFamily } : undefined);
|
|
1905
|
+
}
|
|
1906
|
+
if (decl.kind === "object") {
|
|
1907
|
+
compileObject(decl);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
// Propagate mandatory invoked action requirements
|
|
1911
|
+
let changedInvocations = true;
|
|
1912
|
+
let invocationIterations = 0;
|
|
1913
|
+
while (changedInvocations && invocationIterations < 32) {
|
|
1914
|
+
changedInvocations = false;
|
|
1915
|
+
invocationIterations++;
|
|
1916
|
+
for (const inst of document.instruments) {
|
|
1917
|
+
for (const action of Object.values(inst.actions)) {
|
|
1918
|
+
for (const call of action.invoke ?? []) {
|
|
1919
|
+
if (call.guard)
|
|
1920
|
+
continue;
|
|
1921
|
+
let targetIds = [];
|
|
1922
|
+
if ("instrument" in call) {
|
|
1923
|
+
targetIds = [call.instrument];
|
|
1924
|
+
}
|
|
1925
|
+
else if ("selection" in call) {
|
|
1926
|
+
targetIds = Array.isArray(call.selection.instrument)
|
|
1927
|
+
? call.selection.instrument
|
|
1928
|
+
: [call.selection.instrument];
|
|
1929
|
+
}
|
|
1930
|
+
else if ("reference" in call) {
|
|
1931
|
+
const refField = resolveField(document, inst, call.reference, action.input, action);
|
|
1932
|
+
if (refField?.type === "ref" &&
|
|
1933
|
+
refField.targetKind === "instrument") {
|
|
1934
|
+
targetIds = Array.isArray(refField.target)
|
|
1935
|
+
? refField.target
|
|
1936
|
+
: [refField.target];
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
for (const targetId of targetIds) {
|
|
1940
|
+
const targetInst = document.instruments.find((i) => i.id === targetId);
|
|
1941
|
+
if (!targetInst)
|
|
1942
|
+
continue;
|
|
1943
|
+
if (inst.subject &&
|
|
1944
|
+
targetInst.subject &&
|
|
1945
|
+
inst.subject === targetInst.subject) {
|
|
1946
|
+
const targetAction = targetInst.actions[call.action];
|
|
1947
|
+
if (!targetAction?.subject)
|
|
1948
|
+
continue;
|
|
1949
|
+
if (!action.subject) {
|
|
1950
|
+
action.subject = { requirements: [], adapters: [] };
|
|
1951
|
+
}
|
|
1952
|
+
for (const adapter of targetAction.subject.adapters) {
|
|
1953
|
+
if (!action.subject.adapters.some((existing) => existing.binding === adapter.binding)) {
|
|
1954
|
+
action.subject.adapters.push(adapter);
|
|
1955
|
+
changedInvocations = true;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
for (const targetReq of targetAction.subject.requirements) {
|
|
1959
|
+
const targetObjFieldName = targetReq.objectField ?? targetReq.field.name;
|
|
1960
|
+
const existing = action.subject.requirements.find((cr) => (cr.objectField ?? cr.field.name) === targetObjFieldName);
|
|
1961
|
+
if (!existing) {
|
|
1962
|
+
const inherited = {
|
|
1963
|
+
field: { ...targetReq.field, name: targetObjFieldName },
|
|
1964
|
+
};
|
|
1965
|
+
action.subject.requirements.push(inherited);
|
|
1966
|
+
requirementOrigins.set(inherited, requirementOrigins.get(targetReq));
|
|
1967
|
+
changedInvocations = true;
|
|
1968
|
+
}
|
|
1969
|
+
else if (!sameObjectField(existing.field, targetReq.field)) {
|
|
1970
|
+
const first = requirementOrigins.get(existing);
|
|
1971
|
+
const second = requirementOrigins.get(targetReq);
|
|
1972
|
+
throw new CompileFailure({
|
|
1973
|
+
code: "subject_field_conflict",
|
|
1974
|
+
message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
|
|
1975
|
+
fix: "ensure compatible requirement definitions across invoked actions",
|
|
1976
|
+
span: first.span,
|
|
1977
|
+
source: first.source,
|
|
1978
|
+
related: [first, second],
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
for (const kind of document.objects) {
|
|
1989
|
+
const origins = new Map(kind.fields.map((field) => [
|
|
1990
|
+
field.name,
|
|
1991
|
+
`authored field ${kind.id}.${field.name}`,
|
|
1992
|
+
]));
|
|
1993
|
+
for (const instrument of document.instruments.filter((item) => item.subject === kind.id)) {
|
|
1994
|
+
for (const name of instrument.actionOrder) {
|
|
1995
|
+
for (const requirement of instrument.actions[name].subject
|
|
1996
|
+
?.requirements ?? []) {
|
|
1997
|
+
const field = {
|
|
1998
|
+
...requirement.field,
|
|
1999
|
+
name: requirement.objectField ?? requirement.field.name,
|
|
2000
|
+
};
|
|
2001
|
+
const previous = kind.fields.find((item) => item.name === field.name);
|
|
2002
|
+
const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
|
|
2003
|
+
if (previous && !sameObjectField(previous, field)) {
|
|
2004
|
+
failWithCode(objects.get(kind.id), "subject_field_conflict", `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`, "rename fields with different meanings");
|
|
2005
|
+
}
|
|
2006
|
+
if (!previous) {
|
|
2007
|
+
kind.fields.push(field);
|
|
2008
|
+
origins.set(field.name, origin);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
for (const column of kind.columns) {
|
|
2014
|
+
if (!kind.fields.some((field) => field.name === column))
|
|
2015
|
+
failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
|
|
980
2016
|
}
|
|
981
2017
|
}
|
|
982
2018
|
const eliminatedStates = new Map();
|
|
@@ -1049,13 +2085,36 @@ export function compile(source, options = {}) {
|
|
|
1049
2085
|
continue;
|
|
1050
2086
|
const existing = document.instruments.some((inst) => Object.values(inst.actions).some((action) => action.approval?.action === decision.action &&
|
|
1051
2087
|
action.approval.party === decision.party &&
|
|
1052
|
-
inst.fields.some((field) =>
|
|
1053
|
-
field.
|
|
1054
|
-
|
|
2088
|
+
inst.fields.some((field) => {
|
|
2089
|
+
if (field.type !== "ref" ||
|
|
2090
|
+
field.target !== decision.target ||
|
|
2091
|
+
`self.${field.name}` !== action.approval?.target)
|
|
2092
|
+
return false;
|
|
2093
|
+
const [root, name, ...tail] = decision.protectedRequest.split(".");
|
|
2094
|
+
const input = name && action.approval.input[name];
|
|
2095
|
+
const request = root === "self"
|
|
2096
|
+
? [action.approval.target, name, ...tail]
|
|
2097
|
+
.filter(Boolean)
|
|
2098
|
+
.join(".")
|
|
2099
|
+
: root === "input" && name
|
|
2100
|
+
? input && "field" in input
|
|
2101
|
+
? [input.field, ...tail].join(".")
|
|
2102
|
+
: materialApprovals.has(action)
|
|
2103
|
+
? [`self.material_${name}`, ...tail].join(".")
|
|
2104
|
+
: undefined
|
|
2105
|
+
: undefined;
|
|
2106
|
+
return (request !== undefined &&
|
|
2107
|
+
action.approval.protectedRequest === request);
|
|
2108
|
+
})));
|
|
1055
2109
|
if (existing)
|
|
1056
2110
|
continue;
|
|
1057
2111
|
if (document.instruments.some((inst) => inst.id === id))
|
|
1058
2112
|
fail({ span: decision.origin }, `implicit decision name ${id} is already used`, "rename the conflicting object");
|
|
2113
|
+
const protectedRequest = decision.protectedRequest === "self"
|
|
2114
|
+
? "self.target"
|
|
2115
|
+
: decision.protectedRequest.startsWith("self.")
|
|
2116
|
+
? `self.target.${decision.protectedRequest.slice(5)}`
|
|
2117
|
+
: fail({ span: decision.origin }, "implicit approval needs a stored protected request", "declare an explicit decision for an input-based protected request");
|
|
1059
2118
|
addInstrument(template, id, {
|
|
1060
2119
|
kind: "block",
|
|
1061
2120
|
span: decision.origin,
|
|
@@ -1063,16 +2122,27 @@ export function compile(source, options = {}) {
|
|
|
1063
2122
|
for: decision.target,
|
|
1064
2123
|
approved_by: decision.party,
|
|
1065
2124
|
action: decision.action,
|
|
2125
|
+
protected_request: protectedRequest,
|
|
1066
2126
|
}).map(([key, value]) => ({
|
|
1067
2127
|
key,
|
|
1068
2128
|
value: {
|
|
1069
|
-
kind: key === "action"
|
|
2129
|
+
kind: key === "action" || key === "protected_request"
|
|
2130
|
+
? "text"
|
|
2131
|
+
: "name",
|
|
1070
2132
|
value,
|
|
1071
2133
|
span: decision.origin,
|
|
1072
2134
|
},
|
|
1073
2135
|
span: decision.origin,
|
|
1074
2136
|
})),
|
|
1075
|
-
}, decision.origin)
|
|
2137
|
+
}, decision.origin, new Map(), new Set(), new Map(), document.instruments.find((instrument) => instrument.id === decision.target)?.subject
|
|
2138
|
+
? {
|
|
2139
|
+
subjectKindId: document.instruments.find((instrument) => instrument.id === decision.target).subject,
|
|
2140
|
+
attachmentName: id,
|
|
2141
|
+
parties: {},
|
|
2142
|
+
renames: new Map(),
|
|
2143
|
+
exposed: new Map(),
|
|
2144
|
+
}
|
|
2145
|
+
: undefined);
|
|
1076
2146
|
}
|
|
1077
2147
|
}
|
|
1078
2148
|
// Material approval fields are inferred from the selected action's typed input.
|
|
@@ -1135,7 +2205,7 @@ export function compile(source, options = {}) {
|
|
|
1135
2205
|
.reverse()
|
|
1136
2206
|
.find((o) => i.path.startsWith(o.path));
|
|
1137
2207
|
return diagnostic({
|
|
1138
|
-
code: "HSX1601",
|
|
2208
|
+
code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
|
|
1139
2209
|
message: `${i.path}: ${i.message}`,
|
|
1140
2210
|
fix: i.fix,
|
|
1141
2211
|
span: origin?.span ?? program.span,
|