@hyperscale0/hsx 3.3.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 +4 -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 +1162 -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 +1588 -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/src/compile.ts
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
|
+
import { hash as sha256 } from "fast-sha256";
|
|
1
2
|
import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
|
|
2
3
|
import {
|
|
3
4
|
validateUdl,
|
|
5
|
+
resolveField,
|
|
6
|
+
sameObjectField,
|
|
7
|
+
subjectPartyRoles,
|
|
8
|
+
RESERVED_OBJECT_NAMES,
|
|
9
|
+
type UdlObjectAttachment,
|
|
10
|
+
type AttachmentPartyBinding,
|
|
11
|
+
type SubjectPartyRole,
|
|
12
|
+
udlObjectFieldSchema,
|
|
4
13
|
type UdlAction,
|
|
14
|
+
type UdlActionSubject,
|
|
15
|
+
type UdlAdapterSubjectSnapshot,
|
|
5
16
|
type UdlCalculation,
|
|
6
17
|
type UdlDocument,
|
|
18
|
+
type UdlFamily,
|
|
7
19
|
type UdlField,
|
|
8
20
|
type UdlInstrument,
|
|
21
|
+
type UdlObjectField,
|
|
22
|
+
type UdlSubjectRequirement,
|
|
9
23
|
type UdlValue,
|
|
10
24
|
} from "@hyperscale0/udl";
|
|
11
25
|
import { tunableBounds } from "./tunables.ts";
|
|
12
26
|
import { parseProgram } from "./parse.ts";
|
|
13
27
|
import {
|
|
14
28
|
lineColAt,
|
|
29
|
+
type AssignmentDecl,
|
|
15
30
|
type BlockExpr,
|
|
16
31
|
type Diagnostic,
|
|
32
|
+
type Entry,
|
|
17
33
|
type Expr,
|
|
18
34
|
type InstrumentDecl,
|
|
19
35
|
type ObjectDecl,
|
|
@@ -21,6 +37,8 @@ import {
|
|
|
21
37
|
} from "./ast.ts";
|
|
22
38
|
import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
|
|
23
39
|
|
|
40
|
+
import type { ProviderAdapter } from "@hyperscale0/adl";
|
|
41
|
+
|
|
24
42
|
export interface CompileDiagnostic extends Diagnostic {
|
|
25
43
|
line: number;
|
|
26
44
|
column: number;
|
|
@@ -31,8 +49,13 @@ export interface CompileOriginMapEntry {
|
|
|
31
49
|
path: string;
|
|
32
50
|
span: Span & { line: number; column: number };
|
|
33
51
|
}
|
|
52
|
+
export interface AdapterBindingTarget {
|
|
53
|
+
adapter: ProviderAdapter;
|
|
54
|
+
operation: string;
|
|
55
|
+
}
|
|
34
56
|
export interface CompileOptions {
|
|
35
57
|
standardLibrary?: StandardLibrary;
|
|
58
|
+
adapterRegistry?: Readonly<Record<string, AdapterBindingTarget>>;
|
|
36
59
|
}
|
|
37
60
|
export interface CompileResult {
|
|
38
61
|
verdict: "valid" | "invalid";
|
|
@@ -48,9 +71,154 @@ class CompileFailure extends Error {
|
|
|
48
71
|
super(diagnostic.message);
|
|
49
72
|
}
|
|
50
73
|
}
|
|
51
|
-
function fail(
|
|
52
|
-
|
|
74
|
+
function fail(
|
|
75
|
+
expr: { span: Span; source?: string },
|
|
76
|
+
message: string,
|
|
77
|
+
fix: string,
|
|
78
|
+
): never {
|
|
79
|
+
return failWithCode(expr, "HSX1001", message, fix);
|
|
80
|
+
}
|
|
81
|
+
function failWithCode(
|
|
82
|
+
expr: { span: Span; source?: string },
|
|
83
|
+
code: string,
|
|
84
|
+
message: string,
|
|
85
|
+
fix: string,
|
|
86
|
+
): never {
|
|
87
|
+
throw new CompileFailure({
|
|
88
|
+
code,
|
|
89
|
+
message,
|
|
90
|
+
fix,
|
|
91
|
+
span: expr.span,
|
|
92
|
+
...(expr.source ? { source: expr.source } : {}),
|
|
93
|
+
});
|
|
53
94
|
}
|
|
95
|
+
function lowerFieldShape(
|
|
96
|
+
row: Entry,
|
|
97
|
+
resolveExpr: (e: Expr) => Expr = (e) => e,
|
|
98
|
+
): Record<string, unknown> {
|
|
99
|
+
let rawValue = row.value.kind === "default" ? row.value.type : row.value;
|
|
100
|
+
let isSensitive = false;
|
|
101
|
+
if (rawValue.kind === "call" && rawValue.name === "sensitive") {
|
|
102
|
+
isSensitive = true;
|
|
103
|
+
rawValue = rawValue.args[0]!;
|
|
104
|
+
}
|
|
105
|
+
const t = rawValue;
|
|
106
|
+
if (t.kind === "block") {
|
|
107
|
+
const b = entries(t);
|
|
108
|
+
if (b.has("family") || b.has("target") || b.has("instrument")) {
|
|
109
|
+
const tgt = b.get("instrument") ?? b.get("target");
|
|
110
|
+
let target: string | string[] | undefined;
|
|
111
|
+
if (tgt) {
|
|
112
|
+
const resolved = resolveExpr(tgt);
|
|
113
|
+
if (resolved.kind === "list") {
|
|
114
|
+
const items = resolved.items.map((i) => text(resolveExpr(i)));
|
|
115
|
+
target = items.length === 1 ? items[0] : items;
|
|
116
|
+
} else {
|
|
117
|
+
target = text(resolved);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
name: row.key,
|
|
122
|
+
type: "ref",
|
|
123
|
+
targetKind: "instrument",
|
|
124
|
+
...(target !== undefined ? { target } : {}),
|
|
125
|
+
...(isSensitive ? { sensitive: true } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
130
|
+
const f: Record<string, unknown> = {
|
|
131
|
+
name: row.key,
|
|
132
|
+
type,
|
|
133
|
+
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
134
|
+
...(isSensitive ? { sensitive: true } : {}),
|
|
135
|
+
};
|
|
136
|
+
if (type === "enum" && t.kind === "call") {
|
|
137
|
+
f.values = t.args.map(text);
|
|
138
|
+
}
|
|
139
|
+
if (type === "text" && t.kind === "call") {
|
|
140
|
+
if (t.args.length < 2 || t.args.length > 3) {
|
|
141
|
+
fail(
|
|
142
|
+
t,
|
|
143
|
+
"bounded text needs length bounds and an optional pattern",
|
|
144
|
+
"write text(1, 80)",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
f.minLength = literal(resolveExpr(t.args[0]!));
|
|
148
|
+
f.maxLength = literal(resolveExpr(t.args[1]!));
|
|
149
|
+
if (t.args[2]) f.pattern = literal(resolveExpr(t.args[2]));
|
|
150
|
+
}
|
|
151
|
+
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
152
|
+
if (t.args.length !== 2) {
|
|
153
|
+
fail(
|
|
154
|
+
t,
|
|
155
|
+
"bounded fields need a minimum and maximum",
|
|
156
|
+
"write integer(1, 12) or money(0 SAR, 100 SAR)",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
f.minimum = literal(resolveExpr(t.args[0]!));
|
|
160
|
+
f.maximum = literal(resolveExpr(t.args[1]!));
|
|
161
|
+
}
|
|
162
|
+
if (type === "list" && t.kind === "call") {
|
|
163
|
+
const item = t.args[0]!;
|
|
164
|
+
f.item = item.kind === "type" ? item.name : text(item);
|
|
165
|
+
if (item.kind === "type" && item.name === "ref") {
|
|
166
|
+
f.target = item.target;
|
|
167
|
+
f.targetKind = "object";
|
|
168
|
+
}
|
|
169
|
+
f.maxItems = t.args[1] ? literal(resolveExpr(t.args[1])) : 366;
|
|
170
|
+
}
|
|
171
|
+
if (type === "list" && t.kind === "type") {
|
|
172
|
+
f.item = t.target;
|
|
173
|
+
f.maxItems = 366;
|
|
174
|
+
}
|
|
175
|
+
if (type === "ref") {
|
|
176
|
+
const target =
|
|
177
|
+
t.kind === "type" ? t.target : (f.target as string | undefined);
|
|
178
|
+
if (!target && t.kind !== "block") {
|
|
179
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
180
|
+
}
|
|
181
|
+
f.targetKind = f.targetKind ?? "object";
|
|
182
|
+
if (target) f.target = target;
|
|
183
|
+
}
|
|
184
|
+
return f;
|
|
185
|
+
}
|
|
186
|
+
function lowerObjectField(
|
|
187
|
+
row: Entry,
|
|
188
|
+
resolveExpr: (e: Expr) => Expr = (e) => e,
|
|
189
|
+
): UdlObjectField {
|
|
190
|
+
const f = lowerFieldShape(row, resolveExpr);
|
|
191
|
+
const constant =
|
|
192
|
+
row.value.kind === "default" ? resolveExpr(row.value.value) : undefined;
|
|
193
|
+
if (constant && !(constant.kind === "name" && constant.value === "runtime")) {
|
|
194
|
+
f.value =
|
|
195
|
+
f.type === "enum" && constant.kind === "name"
|
|
196
|
+
? constant.value
|
|
197
|
+
: literal(constant);
|
|
198
|
+
}
|
|
199
|
+
const result = udlObjectFieldSchema.safeParse(f);
|
|
200
|
+
if (!result.success)
|
|
201
|
+
fail(
|
|
202
|
+
row,
|
|
203
|
+
result.error.message,
|
|
204
|
+
"use a UDL object field type and its constraints",
|
|
205
|
+
);
|
|
206
|
+
return result.data;
|
|
207
|
+
}
|
|
208
|
+
function canonicalJson(value: unknown): string {
|
|
209
|
+
if (value === null || typeof value !== "object") {
|
|
210
|
+
return JSON.stringify(value);
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
|
|
214
|
+
}
|
|
215
|
+
const record = value as Record<string, unknown>;
|
|
216
|
+
const keys = Object.keys(record)
|
|
217
|
+
.filter((k) => record[k] !== undefined)
|
|
218
|
+
.sort();
|
|
219
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
54
222
|
const emptyBlock: BlockExpr = {
|
|
55
223
|
kind: "block",
|
|
56
224
|
entries: [],
|
|
@@ -69,9 +237,14 @@ function entries(block: BlockExpr): Map<string, Expr> {
|
|
|
69
237
|
const previous = map.get(row.key);
|
|
70
238
|
if (previous) {
|
|
71
239
|
if (
|
|
72
|
-
![
|
|
73
|
-
|
|
74
|
-
|
|
240
|
+
![
|
|
241
|
+
"requires",
|
|
242
|
+
"moves",
|
|
243
|
+
"invariants",
|
|
244
|
+
"invoke",
|
|
245
|
+
"calculate",
|
|
246
|
+
"expose",
|
|
247
|
+
].includes(row.key)
|
|
75
248
|
)
|
|
76
249
|
fail(row, `duplicate ${row.key}`, "keep one value for this name");
|
|
77
250
|
const items = (expr: Expr) =>
|
|
@@ -212,12 +385,13 @@ export function compile(
|
|
|
212
385
|
options: CompileOptions = {},
|
|
213
386
|
): CompileResult {
|
|
214
387
|
const parsed = parseProgram(source);
|
|
388
|
+
const sources = new Map([["program", source]]);
|
|
215
389
|
const diagnostic = (
|
|
216
390
|
d: Diagnostic,
|
|
217
391
|
stage: CompileDiagnostic["stage"],
|
|
218
392
|
): CompileDiagnostic => ({
|
|
219
393
|
...d,
|
|
220
|
-
...lineColAt(source, d.span.start),
|
|
394
|
+
...lineColAt(sources.get(d.source ?? "program") ?? source, d.span.start),
|
|
221
395
|
severity: "error",
|
|
222
396
|
stage,
|
|
223
397
|
});
|
|
@@ -241,6 +415,12 @@ export function compile(
|
|
|
241
415
|
"write currency SAR or omit currency",
|
|
242
416
|
);
|
|
243
417
|
const templates = new Map<string, InstrumentDecl>();
|
|
418
|
+
const declarationSources = new Map<InstrumentDecl, string>();
|
|
419
|
+
const declarationExportPaths = new Map<InstrumentDecl, string>();
|
|
420
|
+
const requirementOrigins = new Map<
|
|
421
|
+
UdlSubjectRequirement,
|
|
422
|
+
{ source: string; span: Span; message: string }
|
|
423
|
+
>();
|
|
244
424
|
const used = new Set<string>();
|
|
245
425
|
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
246
426
|
if (used.has(use.name))
|
|
@@ -255,6 +435,7 @@ export function compile(
|
|
|
255
435
|
).source(use.name);
|
|
256
436
|
if (!content)
|
|
257
437
|
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
438
|
+
sources.set(use.name, content);
|
|
258
439
|
const header = parseProgram(content);
|
|
259
440
|
if (
|
|
260
441
|
header.diagnostics.length ||
|
|
@@ -266,12 +447,39 @@ export function compile(
|
|
|
266
447
|
`header ${use.name} is malformed`,
|
|
267
448
|
"repair the header source before compiling",
|
|
268
449
|
);
|
|
450
|
+
const registerTemplates = (
|
|
451
|
+
parentDecl: InstrumentDecl,
|
|
452
|
+
prefix: string,
|
|
453
|
+
exportPath: string,
|
|
454
|
+
) => {
|
|
455
|
+
templates.set(prefix, parentDecl);
|
|
456
|
+
declarationSources.set(parentDecl, use.name);
|
|
457
|
+
declarationExportPaths.set(parentDecl, exportPath);
|
|
458
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
459
|
+
for (const [recName, recBlock] of recs) {
|
|
460
|
+
const recDecl: InstrumentDecl = {
|
|
461
|
+
kind: "instrument",
|
|
462
|
+
name: recName,
|
|
463
|
+
parameters: [],
|
|
464
|
+
body: asBlock(recBlock),
|
|
465
|
+
span: parentDecl.span,
|
|
466
|
+
};
|
|
467
|
+
registerTemplates(
|
|
468
|
+
recDecl,
|
|
469
|
+
`${prefix}.${recName}`,
|
|
470
|
+
`${exportPath}.${recName}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
269
474
|
for (const decl of header.program.decls)
|
|
270
|
-
if (decl.kind === "instrument")
|
|
271
|
-
|
|
475
|
+
if (decl.kind === "instrument") {
|
|
476
|
+
registerTemplates(decl, `${use.name}.${decl.name}`, decl.name);
|
|
477
|
+
}
|
|
272
478
|
}
|
|
479
|
+
for (const decl of program.decls)
|
|
480
|
+
if (decl.kind === "instrument") templates.set(decl.name, decl);
|
|
273
481
|
const document: UdlDocument = {
|
|
274
|
-
udl:
|
|
482
|
+
udl: 4,
|
|
275
483
|
version: 1,
|
|
276
484
|
product: program.name,
|
|
277
485
|
title: program.title,
|
|
@@ -282,9 +490,12 @@ export function compile(
|
|
|
282
490
|
programFines: { kind: "business", role: "fine_payable" },
|
|
283
491
|
programCosts: { kind: "business", role: "cost_recovery" },
|
|
284
492
|
},
|
|
493
|
+
objects: [],
|
|
285
494
|
instruments: [],
|
|
286
495
|
};
|
|
287
496
|
const objects = new Map<string, ObjectDecl>();
|
|
497
|
+
const assignments = new Map<string, AssignmentDecl>();
|
|
498
|
+
const attachmentSubjects = new Map<string, string>();
|
|
288
499
|
const names = new Set<string>();
|
|
289
500
|
for (const decl of program.decls) {
|
|
290
501
|
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
@@ -296,8 +507,31 @@ export function compile(
|
|
|
296
507
|
"give this declaration a distinct name",
|
|
297
508
|
);
|
|
298
509
|
names.add(decl.name);
|
|
299
|
-
if (decl.kind === "object")
|
|
510
|
+
if (decl.kind === "object") {
|
|
511
|
+
objects.set(decl.name, decl);
|
|
512
|
+
for (const entry of decl.body.entries) {
|
|
513
|
+
const match = /^attach\s+(\w+)\s*=\s*(.+)$/.exec(entry.key);
|
|
514
|
+
if (!match) continue;
|
|
515
|
+
const name = `${decl.name}_${match[1]}`;
|
|
516
|
+
assignments.set(name, {
|
|
517
|
+
kind: "assignment",
|
|
518
|
+
name,
|
|
519
|
+
target: match[2]!,
|
|
520
|
+
body: asBlock(entry.value),
|
|
521
|
+
span: entry.span,
|
|
522
|
+
});
|
|
523
|
+
attachmentSubjects.set(name, decl.name);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (decl.kind === "assignment") assignments.set(decl.name, decl);
|
|
300
527
|
if (decl.kind === "party") {
|
|
528
|
+
if (subjectPartyRoles.includes(decl.name as SubjectPartyRole))
|
|
529
|
+
failWithCode(
|
|
530
|
+
decl,
|
|
531
|
+
"party_name_reserved",
|
|
532
|
+
`${decl.name} is a reserved subject role`,
|
|
533
|
+
"choose a party name other than owner, actor or operator",
|
|
534
|
+
);
|
|
301
535
|
if (!["person", "business", "staff"].includes(decl.partyKind))
|
|
302
536
|
fail(
|
|
303
537
|
decl,
|
|
@@ -314,8 +548,81 @@ export function compile(
|
|
|
314
548
|
const materialApprovals = new Set<UdlAction>();
|
|
315
549
|
const implicitDecisions = new Map<
|
|
316
550
|
string,
|
|
317
|
-
{
|
|
551
|
+
{
|
|
552
|
+
target: string;
|
|
553
|
+
action: string;
|
|
554
|
+
party: string;
|
|
555
|
+
protectedRequest: string;
|
|
556
|
+
origin: Span;
|
|
557
|
+
}
|
|
318
558
|
>();
|
|
559
|
+
const resolveFamily = (
|
|
560
|
+
rawPath: string,
|
|
561
|
+
expr: { span: Span; source?: string },
|
|
562
|
+
required = true,
|
|
563
|
+
): UdlFamily | undefined => {
|
|
564
|
+
const parts = rawPath.split(".");
|
|
565
|
+
if (parts.length < 2) {
|
|
566
|
+
if (!required) return undefined;
|
|
567
|
+
failWithCode(
|
|
568
|
+
expr,
|
|
569
|
+
"HSX1001",
|
|
570
|
+
`invalid family ${rawPath}`,
|
|
571
|
+
"use module.instrument or module.instrument.record",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const moduleName = parts[0]!;
|
|
575
|
+
const exportPath = parts.slice(1).join(".");
|
|
576
|
+
const targetTemplate = templates.get(rawPath);
|
|
577
|
+
if (!targetTemplate) {
|
|
578
|
+
if (!required) return undefined;
|
|
579
|
+
failWithCode(
|
|
580
|
+
expr,
|
|
581
|
+
"HSX1001",
|
|
582
|
+
`unknown family declaration ${rawPath}`,
|
|
583
|
+
"choose a declared standard instrument",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const topTemplate = templates.get(`${moduleName}.${parts[1]!}`);
|
|
587
|
+
if (!topTemplate) {
|
|
588
|
+
if (!required) return undefined;
|
|
589
|
+
failWithCode(
|
|
590
|
+
expr,
|
|
591
|
+
"HSX1001",
|
|
592
|
+
`unknown family declaration ${moduleName}.${parts[1]!}`,
|
|
593
|
+
"choose a declared standard instrument",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const topBody = entries(topTemplate.body);
|
|
597
|
+
let revision: number | undefined;
|
|
598
|
+
if (topBody.has("familyRevision")) {
|
|
599
|
+
const val = literal(topBody.get("familyRevision")!);
|
|
600
|
+
if (typeof val === "number") revision = val;
|
|
601
|
+
}
|
|
602
|
+
let currentBody = topTemplate.body;
|
|
603
|
+
for (const recName of parts.slice(2)) {
|
|
604
|
+
const recs = entries(asBlock(entries(currentBody).get("records")));
|
|
605
|
+
const child = recs.get(recName);
|
|
606
|
+
if (child) {
|
|
607
|
+
currentBody = asBlock(child);
|
|
608
|
+
const cBody = entries(currentBody);
|
|
609
|
+
if (cBody.has("familyRevision")) {
|
|
610
|
+
const val = literal(cBody.get("familyRevision")!);
|
|
611
|
+
if (typeof val === "number") revision = val;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (revision === undefined) {
|
|
616
|
+
if (!required) return undefined;
|
|
617
|
+
failWithCode(
|
|
618
|
+
expr,
|
|
619
|
+
"HSX1001",
|
|
620
|
+
`declaration ${rawPath} has no familyRevision declared`,
|
|
621
|
+
"declare familyRevision on the standard instrument",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return { module: moduleName, exportPath, revision };
|
|
625
|
+
};
|
|
319
626
|
const addInstrument = (
|
|
320
627
|
decl: InstrumentDecl,
|
|
321
628
|
id: string,
|
|
@@ -324,7 +631,169 @@ export function compile(
|
|
|
324
631
|
inherited = new Map<string, Expr>(),
|
|
325
632
|
inheritedApprovers = new Set<string>(),
|
|
326
633
|
inheritedEnums = new Map<string, string[]>(),
|
|
634
|
+
attachmentInfo?: {
|
|
635
|
+
subjectKindId: string;
|
|
636
|
+
attachmentName: string;
|
|
637
|
+
renames: Map<string, string>;
|
|
638
|
+
exposed: Map<string, string>;
|
|
639
|
+
parties: Record<string, AttachmentPartyBinding>;
|
|
640
|
+
},
|
|
641
|
+
familyDeclaration?: {
|
|
642
|
+
module: string;
|
|
643
|
+
exportPath: string;
|
|
644
|
+
revision?: number | undefined;
|
|
645
|
+
},
|
|
327
646
|
) => {
|
|
647
|
+
const resolveFamilyInstruments = (
|
|
648
|
+
family: UdlFamily,
|
|
649
|
+
currentInstId?: string,
|
|
650
|
+
expr?: { span: Span; source?: string },
|
|
651
|
+
): string[] => {
|
|
652
|
+
const found = new Set<string>();
|
|
653
|
+
if (currentInstId && familyDeclaration) {
|
|
654
|
+
if (
|
|
655
|
+
family.module === familyDeclaration.module &&
|
|
656
|
+
family.exportPath.startsWith(`${familyDeclaration.exportPath}.`)
|
|
657
|
+
) {
|
|
658
|
+
const sub = family.exportPath
|
|
659
|
+
.slice(familyDeclaration.exportPath.length + 1)
|
|
660
|
+
.replaceAll(".", "_");
|
|
661
|
+
found.add(`${currentInstId}_${sub}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const topDeclName = family.exportPath.split(".")[0]!;
|
|
665
|
+
const subRecordPath = family.exportPath.includes(".")
|
|
666
|
+
? family.exportPath.slice(topDeclName.length + 1).replaceAll(".", "_")
|
|
667
|
+
: undefined;
|
|
668
|
+
for (const asgn of assignments.values()) {
|
|
669
|
+
if (asgn.target === `${family.module}.${topDeclName}`) {
|
|
670
|
+
if (subRecordPath) {
|
|
671
|
+
found.add(`${asgn.name}_${subRecordPath}`);
|
|
672
|
+
} else {
|
|
673
|
+
found.add(asgn.name);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const inst of document.instruments) {
|
|
678
|
+
if (
|
|
679
|
+
inst.family &&
|
|
680
|
+
inst.family.module === family.module &&
|
|
681
|
+
inst.family.exportPath === family.exportPath &&
|
|
682
|
+
inst.family.revision === family.revision
|
|
683
|
+
) {
|
|
684
|
+
found.add(inst.id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const result = [...found];
|
|
688
|
+
if (result.length === 0 && expr) {
|
|
689
|
+
failWithCode(
|
|
690
|
+
expr,
|
|
691
|
+
"HSX1001",
|
|
692
|
+
`no instruments found for family ${family.module}.${family.exportPath}`,
|
|
693
|
+
"declare an attachment matching this family",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const resolveChildExportPath = (
|
|
700
|
+
parentDecl: InstrumentDecl,
|
|
701
|
+
suffix: string,
|
|
702
|
+
): string | undefined => {
|
|
703
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
704
|
+
for (const [recName] of recs) {
|
|
705
|
+
if (suffix === recName) return recName;
|
|
706
|
+
}
|
|
707
|
+
for (const [recName, recBlock] of recs) {
|
|
708
|
+
if (suffix.startsWith(`${recName}_`)) {
|
|
709
|
+
const rest = resolveChildExportPath(
|
|
710
|
+
{
|
|
711
|
+
kind: "instrument",
|
|
712
|
+
name: recName,
|
|
713
|
+
parameters: [],
|
|
714
|
+
body: asBlock(recBlock),
|
|
715
|
+
span: parentDecl.span,
|
|
716
|
+
},
|
|
717
|
+
suffix.slice(recName.length + 1),
|
|
718
|
+
);
|
|
719
|
+
if (rest) return `${recName}.${rest}`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return undefined;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
|
|
726
|
+
const existing = document.instruments.find((i) => i.id === targetId);
|
|
727
|
+
if (existing?.family) return existing.family;
|
|
728
|
+
|
|
729
|
+
if (targetId.startsWith(`${id}_`) && familyDeclaration) {
|
|
730
|
+
const sub = resolveChildExportPath(
|
|
731
|
+
decl,
|
|
732
|
+
targetId.slice(id.length + 1),
|
|
733
|
+
);
|
|
734
|
+
if (sub) {
|
|
735
|
+
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
736
|
+
try {
|
|
737
|
+
return resolveFamily(
|
|
738
|
+
`${familyDeclaration.module}.${fullExport}`,
|
|
739
|
+
{ span: origin },
|
|
740
|
+
false,
|
|
741
|
+
);
|
|
742
|
+
} catch {}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
for (const [asgnName, asgn] of assignments) {
|
|
747
|
+
if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
|
|
748
|
+
const tmpl = templates.get(asgn.target);
|
|
749
|
+
if (!tmpl) continue;
|
|
750
|
+
const mod = declarationSources.get(tmpl);
|
|
751
|
+
if (!mod || mod === "program") continue;
|
|
752
|
+
if (targetId === asgnName) {
|
|
753
|
+
try {
|
|
754
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
755
|
+
} catch {}
|
|
756
|
+
} else {
|
|
757
|
+
const sub = resolveChildExportPath(
|
|
758
|
+
tmpl,
|
|
759
|
+
targetId.slice(asgnName.length + 1),
|
|
760
|
+
);
|
|
761
|
+
if (sub) {
|
|
762
|
+
const fullTarget = `${asgn.target}.${sub}`;
|
|
763
|
+
try {
|
|
764
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
765
|
+
} catch {}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return undefined;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const checkTargetFamily = (
|
|
774
|
+
targetIds: string | string[],
|
|
775
|
+
expectedFamily: UdlFamily,
|
|
776
|
+
expr: { span: Span; source?: string },
|
|
777
|
+
): void => {
|
|
778
|
+
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
779
|
+
for (const tid of ids) {
|
|
780
|
+
const fam = getInstrumentFamily(tid);
|
|
781
|
+
if (
|
|
782
|
+
!fam ||
|
|
783
|
+
fam.module !== expectedFamily.module ||
|
|
784
|
+
fam.exportPath !== expectedFamily.exportPath ||
|
|
785
|
+
fam.revision !== expectedFamily.revision
|
|
786
|
+
) {
|
|
787
|
+
failWithCode(
|
|
788
|
+
expr,
|
|
789
|
+
"HSX1001",
|
|
790
|
+
`target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`,
|
|
791
|
+
"ensure target instrument matches the declared family",
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
328
797
|
const enums = new Map(inheritedEnums);
|
|
329
798
|
for (const parameter of decl.parameters) {
|
|
330
799
|
const type =
|
|
@@ -335,18 +804,44 @@ export function compile(
|
|
|
335
804
|
enums.set(parameter.key, type.args.map(text));
|
|
336
805
|
}
|
|
337
806
|
const approvers = new Set(inheritedApprovers);
|
|
338
|
-
const supplied =
|
|
807
|
+
const supplied = new Map<string, Expr>();
|
|
808
|
+
for (const entry of arguments_.entries) {
|
|
809
|
+
if (supplied.has(entry.key))
|
|
810
|
+
fail(
|
|
811
|
+
entry,
|
|
812
|
+
`duplicate tunable ${entry.key}`,
|
|
813
|
+
"supply each parameter once",
|
|
814
|
+
);
|
|
815
|
+
supplied.set(entry.key, entry.value);
|
|
816
|
+
}
|
|
339
817
|
const environment = new Map<string, Expr>(inherited);
|
|
340
818
|
for (const param of decl.parameters) {
|
|
341
819
|
const type =
|
|
342
820
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
343
821
|
const fallback =
|
|
344
822
|
param.value.kind === "default" ? param.value.value : undefined;
|
|
345
|
-
const
|
|
823
|
+
const typeName =
|
|
824
|
+
type.kind === "type" || type.kind === "call" ? type.name : text(type);
|
|
825
|
+
const partyParameter =
|
|
826
|
+
attachmentInfo && (typeName === "party" || typeName === "approval");
|
|
827
|
+
const byName: Expr | undefined =
|
|
828
|
+
partyParameter &&
|
|
829
|
+
(subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
|
|
830
|
+
document.parties[param.key])
|
|
831
|
+
? { kind: "name", value: param.key, span: origin }
|
|
832
|
+
: undefined;
|
|
833
|
+
const actual =
|
|
834
|
+
supplied.get(param.key) ??
|
|
835
|
+
byName ??
|
|
836
|
+
(fallback && {
|
|
837
|
+
...fallback,
|
|
838
|
+
source: declarationSources.get(decl) ?? "program",
|
|
839
|
+
});
|
|
346
840
|
if (!actual) {
|
|
347
841
|
if (type.kind === "type" && type.optional) continue;
|
|
348
|
-
|
|
842
|
+
failWithCode(
|
|
349
843
|
{ span: origin },
|
|
844
|
+
partyParameter ? "subject_party_unbound" : "HSX1001",
|
|
350
845
|
`${id} needs ${param.key}`,
|
|
351
846
|
`add ${param.key}: value inside ${id}`,
|
|
352
847
|
);
|
|
@@ -360,7 +855,16 @@ export function compile(
|
|
|
360
855
|
`unknown tunable ${key}`,
|
|
361
856
|
`choose ${decl.parameters.map((p) => p.key).join(", ")}`,
|
|
362
857
|
);
|
|
363
|
-
const
|
|
858
|
+
const isParty = (name: string) =>
|
|
859
|
+
!!document.parties[name] ||
|
|
860
|
+
(!!attachmentInfo &&
|
|
861
|
+
subjectPartyRoles.includes(name as SubjectPartyRole));
|
|
862
|
+
const resolvedParties = new Set<string>();
|
|
863
|
+
const resolve = (
|
|
864
|
+
expr: Expr,
|
|
865
|
+
seen = new Set<string>(),
|
|
866
|
+
partyBinding = false,
|
|
867
|
+
): Expr => {
|
|
364
868
|
if (
|
|
365
869
|
expr.kind === "call" &&
|
|
366
870
|
["object", "all", "party"].includes(expr.name)
|
|
@@ -375,22 +879,33 @@ export function compile(
|
|
|
375
879
|
const matches =
|
|
376
880
|
expr.name === "party"
|
|
377
881
|
? Object.entries(document.parties)
|
|
378
|
-
.filter(
|
|
882
|
+
.filter(
|
|
883
|
+
([name, party]) =>
|
|
884
|
+
party.kind === type && (!partyBinding || names.has(name)),
|
|
885
|
+
)
|
|
379
886
|
.map(([name]) => name)
|
|
380
|
-
: [...
|
|
887
|
+
: [...assignments.values()]
|
|
888
|
+
.filter(
|
|
889
|
+
(assignment) =>
|
|
890
|
+
expr.name === "all" ||
|
|
891
|
+
!attachmentSubjects.has(assignment.name) ||
|
|
892
|
+
attachmentSubjects.get(assignment.name) ===
|
|
893
|
+
attachmentInfo?.subjectKindId,
|
|
894
|
+
)
|
|
381
895
|
.filter(
|
|
382
|
-
(
|
|
383
|
-
|
|
384
|
-
type.startsWith(`${
|
|
896
|
+
(assignment) =>
|
|
897
|
+
assignment.target === type ||
|
|
898
|
+
type.startsWith(`${assignment.target}.`),
|
|
385
899
|
)
|
|
386
900
|
.map(
|
|
387
|
-
(
|
|
388
|
-
|
|
389
|
-
type.slice(
|
|
901
|
+
(assignment) =>
|
|
902
|
+
assignment.name +
|
|
903
|
+
type.slice(assignment.target.length).replaceAll(".", "_"),
|
|
390
904
|
);
|
|
391
905
|
if (expr.name !== "all" && matches.length !== 1)
|
|
392
|
-
|
|
393
|
-
|
|
906
|
+
failWithCode(
|
|
907
|
+
expr,
|
|
908
|
+
partyBinding ? "subject_party_unbound" : "HSX1001",
|
|
394
909
|
`${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
|
|
395
910
|
"declare the required object or supply this tunable explicitly",
|
|
396
911
|
);
|
|
@@ -404,25 +919,46 @@ export function compile(
|
|
|
404
919
|
: items[0]!;
|
|
405
920
|
}
|
|
406
921
|
if (expr.kind !== "name") return expr;
|
|
922
|
+
const binding = environment.get(expr.value);
|
|
923
|
+
if (
|
|
924
|
+
attachmentInfo &&
|
|
925
|
+
subjectPartyRoles.includes(expr.value as SubjectPartyRole) &&
|
|
926
|
+
(!binding ||
|
|
927
|
+
(binding.kind === "name" && binding.value === expr.value))
|
|
928
|
+
)
|
|
929
|
+
return expr;
|
|
930
|
+
if (attachmentInfo) {
|
|
931
|
+
const [local, ...tail] = expr.value.split(".");
|
|
932
|
+
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
933
|
+
if (attachmentSubjects.has(target))
|
|
934
|
+
return { ...expr, value: [target, ...tail].join("_") };
|
|
935
|
+
}
|
|
407
936
|
if (expr.value.startsWith("party.")) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
937
|
+
const [, party, ...members] = expr.value.split(".");
|
|
938
|
+
const binding = environment.get(party!);
|
|
939
|
+
if (binding?.kind === "name" && isParty(binding.value))
|
|
940
|
+
return {
|
|
941
|
+
...expr,
|
|
942
|
+
value: ["party", binding.value, ...members].join("."),
|
|
943
|
+
};
|
|
411
944
|
}
|
|
412
945
|
const [root, ...tail] = expr.value.split(".");
|
|
413
946
|
const bound = environment.get(root!);
|
|
414
947
|
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
415
948
|
return expr;
|
|
416
949
|
if (seen.has(root!))
|
|
417
|
-
return
|
|
950
|
+
return failWithCode(
|
|
418
951
|
expr,
|
|
952
|
+
partyBinding ? "subject_party_unbound" : "HSX1001",
|
|
419
953
|
`cyclic tunable ${root}`,
|
|
420
954
|
"replace the cycle with a literal or declared reference",
|
|
421
955
|
);
|
|
422
956
|
let resolved =
|
|
423
|
-
|
|
957
|
+
resolvedParties.has(root!) ||
|
|
958
|
+
(!partyBinding &&
|
|
959
|
+
(supplied.has(root!) || inherited.has(root!) || enums.has(root!)))
|
|
424
960
|
? bound
|
|
425
|
-
: resolve(bound, new Set([...seen, root!]));
|
|
961
|
+
: resolve(bound, new Set([...seen, root!]), partyBinding);
|
|
426
962
|
for (const key of tail) {
|
|
427
963
|
if (resolved.kind !== "block") return expr;
|
|
428
964
|
const child = entries(resolved).get(key);
|
|
@@ -432,7 +968,7 @@ export function compile(
|
|
|
432
968
|
`missing tunable ${expr.value}`,
|
|
433
969
|
`declare ${key} in ${root}`,
|
|
434
970
|
);
|
|
435
|
-
resolved = resolve(child, new Set([...seen, root!]));
|
|
971
|
+
resolved = resolve(child, new Set([...seen, root!]), partyBinding);
|
|
436
972
|
}
|
|
437
973
|
return resolved;
|
|
438
974
|
};
|
|
@@ -443,7 +979,13 @@ export function compile(
|
|
|
443
979
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
444
980
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
445
981
|
const v =
|
|
446
|
-
supplied.has(param.key) || type === "enum"
|
|
982
|
+
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
983
|
+
? actual
|
|
984
|
+
: resolve(
|
|
985
|
+
actual,
|
|
986
|
+
new Set(),
|
|
987
|
+
!!attachmentInfo && (type === "party" || type === "approval"),
|
|
988
|
+
);
|
|
447
989
|
environment.set(param.key, v);
|
|
448
990
|
if (type === "enum" && t.kind === "call") {
|
|
449
991
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -456,12 +998,35 @@ export function compile(
|
|
|
456
998
|
if (v.kind !== "list")
|
|
457
999
|
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
458
1000
|
} else if (type === "party" || type === "approval") {
|
|
459
|
-
if (v.kind !== "name" || !
|
|
460
|
-
|
|
461
|
-
|
|
1001
|
+
if (v.kind !== "name" || !isParty(v.value))
|
|
1002
|
+
failWithCode(
|
|
1003
|
+
actual,
|
|
1004
|
+
attachmentInfo ? "subject_party_unbound" : "HSX1001",
|
|
462
1005
|
`${param.key} needs a declared party`,
|
|
463
1006
|
"declare a party and use its name here",
|
|
464
1007
|
);
|
|
1008
|
+
const party = document.parties[v.value];
|
|
1009
|
+
if (
|
|
1010
|
+
(type === "approval" && (party?.kind !== "staff" || !party.role)) ||
|
|
1011
|
+
(type === "party" &&
|
|
1012
|
+
(party?.kind === "staff" ||
|
|
1013
|
+
(attachmentInfo && party?.kind === "person")))
|
|
1014
|
+
)
|
|
1015
|
+
failWithCode(
|
|
1016
|
+
actual,
|
|
1017
|
+
"party_kind_mismatch",
|
|
1018
|
+
`${param.key} cannot bind ${v.value}`,
|
|
1019
|
+
type === "approval"
|
|
1020
|
+
? "use a declared staff party with a role"
|
|
1021
|
+
: "use a subject role or declared business",
|
|
1022
|
+
);
|
|
1023
|
+
resolvedParties.add(param.key);
|
|
1024
|
+
if (attachmentInfo)
|
|
1025
|
+
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
|
|
1026
|
+
v.value as SubjectPartyRole,
|
|
1027
|
+
)
|
|
1028
|
+
? { role: v.value as SubjectPartyRole }
|
|
1029
|
+
: { party: v.value };
|
|
465
1030
|
if (type === "approval") approvers.add(text(v));
|
|
466
1031
|
} else if (type === "ref") {
|
|
467
1032
|
const values = v.kind === "list" ? v.items : [v];
|
|
@@ -487,14 +1052,17 @@ export function compile(
|
|
|
487
1052
|
);
|
|
488
1053
|
const [root, ...tail] = value.value.split(".");
|
|
489
1054
|
const obj = objects.get(root!);
|
|
1055
|
+
const assignment = assignments.get(root!);
|
|
1056
|
+
const targetType = obj ? obj.name : assignment?.target;
|
|
490
1057
|
if (
|
|
491
1058
|
(!obj &&
|
|
1059
|
+
!assignment &&
|
|
492
1060
|
!document.instruments.some(
|
|
493
1061
|
(inst) => inst.id === value.value,
|
|
494
1062
|
)) ||
|
|
495
1063
|
(t.kind === "type" &&
|
|
496
1064
|
t.target &&
|
|
497
|
-
[
|
|
1065
|
+
[targetType, ...tail].join(".") !== t.target)
|
|
498
1066
|
)
|
|
499
1067
|
fail(
|
|
500
1068
|
value,
|
|
@@ -590,6 +1158,7 @@ export function compile(
|
|
|
590
1158
|
for (const key of body.keys())
|
|
591
1159
|
if (
|
|
592
1160
|
![
|
|
1161
|
+
"familyRevision",
|
|
593
1162
|
"fields",
|
|
594
1163
|
"lifecycle",
|
|
595
1164
|
"records",
|
|
@@ -606,6 +1175,24 @@ export function compile(
|
|
|
606
1175
|
`unknown instrument clause ${key}`,
|
|
607
1176
|
"use fields, lifecycle, actions, invariants, or records",
|
|
608
1177
|
);
|
|
1178
|
+
if (body.has("familyRevision")) {
|
|
1179
|
+
const revExpr = body.get("familyRevision")!;
|
|
1180
|
+
const revVal = literal(revExpr);
|
|
1181
|
+
if (
|
|
1182
|
+
typeof revVal !== "number" ||
|
|
1183
|
+
!Number.isInteger(revVal) ||
|
|
1184
|
+
revVal <= 0
|
|
1185
|
+
) {
|
|
1186
|
+
fail(
|
|
1187
|
+
revExpr,
|
|
1188
|
+
"familyRevision must be a positive integer",
|
|
1189
|
+
"use a positive integer revision",
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (familyDeclaration) {
|
|
1193
|
+
familyDeclaration.revision = revVal;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
609
1196
|
const records = entries(asBlock(body.get("records")));
|
|
610
1197
|
if (!inherited.size)
|
|
611
1198
|
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
@@ -618,11 +1205,63 @@ export function compile(
|
|
|
618
1205
|
});
|
|
619
1206
|
const fields: UdlField[] = [];
|
|
620
1207
|
const calculations: UdlCalculation[] = [];
|
|
1208
|
+
let currentAction: UdlAction | undefined;
|
|
1209
|
+
let currentActionName: string | undefined;
|
|
621
1210
|
const path = (expr: Expr): string => {
|
|
622
1211
|
const value = resolve(expr);
|
|
623
1212
|
const name = text(value);
|
|
624
|
-
if (
|
|
625
|
-
|
|
1213
|
+
if (isParty(name)) return `party.${name}`;
|
|
1214
|
+
if (name.startsWith("subject.")) {
|
|
1215
|
+
const subField = name.split(".")[1]!;
|
|
1216
|
+
if (currentAction) {
|
|
1217
|
+
const req = currentAction.subject?.requirements.find(
|
|
1218
|
+
(r) => r.field.name === subField,
|
|
1219
|
+
);
|
|
1220
|
+
if (!req) {
|
|
1221
|
+
failWithCode(
|
|
1222
|
+
expr,
|
|
1223
|
+
"subject_field_unknown",
|
|
1224
|
+
`subject.${subField} names no declared subject requirement in action ${currentActionName}`,
|
|
1225
|
+
`declare ${subField} in subject { ... }`,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
} else {
|
|
1229
|
+
const declaredInAction = decl.body.entries.some((e) => {
|
|
1230
|
+
if (!e.key.startsWith("action ")) return false;
|
|
1231
|
+
const subBlock = entries(asBlock(e.value)).get("subject");
|
|
1232
|
+
if (!subBlock) return false;
|
|
1233
|
+
return asBlock(subBlock).entries.some((se) => {
|
|
1234
|
+
if (se.key === subField) return true;
|
|
1235
|
+
if (se.key === "adapter") {
|
|
1236
|
+
const names =
|
|
1237
|
+
se.value.kind === "list"
|
|
1238
|
+
? se.value.items.map(text)
|
|
1239
|
+
: [text(se.value)];
|
|
1240
|
+
return names.some((n) => {
|
|
1241
|
+
const reg = options.adapterRegistry?.[n];
|
|
1242
|
+
if (!reg) return false;
|
|
1243
|
+
const op = reg.adapter.operationMap[reg.operation];
|
|
1244
|
+
return op?.subjectRequirements?.some(
|
|
1245
|
+
(sr) => sr.name === subField,
|
|
1246
|
+
);
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
return false;
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
if (!declaredInAction) {
|
|
1253
|
+
failWithCode(
|
|
1254
|
+
expr,
|
|
1255
|
+
"subject_field_unknown",
|
|
1256
|
+
`subject.${subField} names no declared subject requirement in instrument ${id}`,
|
|
1257
|
+
`declare ${subField} in an action subject { ... }`,
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return /^(self|input|party|subject)\./.test(name)
|
|
1263
|
+
? name
|
|
1264
|
+
: `self.${name}`;
|
|
626
1265
|
};
|
|
627
1266
|
const val = (expr: Expr): UdlValue => {
|
|
628
1267
|
const v = resolve(expr);
|
|
@@ -639,8 +1278,57 @@ export function compile(
|
|
|
639
1278
|
return id;
|
|
640
1279
|
const value = resolve(expr);
|
|
641
1280
|
if (value.kind === "block") {
|
|
1281
|
+
const rawEntries = entries(value);
|
|
1282
|
+
if (
|
|
1283
|
+
(rawEntries.has("family") && !rawEntries.has("kind")) ||
|
|
1284
|
+
((rawEntries.has("states") ||
|
|
1285
|
+
rawEntries.has("reference") ||
|
|
1286
|
+
rawEntries.has("anchor")) &&
|
|
1287
|
+
rawEntries.has("instrument"))
|
|
1288
|
+
) {
|
|
1289
|
+
let famTuple: UdlFamily | undefined;
|
|
1290
|
+
if (rawEntries.has("family")) {
|
|
1291
|
+
const famExpr = rawEntries.get("family")!;
|
|
1292
|
+
const famStr =
|
|
1293
|
+
famExpr.kind === "name" ? famExpr.value : text(famExpr);
|
|
1294
|
+
famTuple = resolveFamily(famStr, famExpr);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
let instrumentVal: unknown;
|
|
1298
|
+
if (rawEntries.has("instrument")) {
|
|
1299
|
+
instrumentVal = data(rawEntries.get("instrument")!);
|
|
1300
|
+
if (famTuple) {
|
|
1301
|
+
checkTargetFamily(
|
|
1302
|
+
instrumentVal as string | string[],
|
|
1303
|
+
famTuple,
|
|
1304
|
+
rawEntries.get("instrument")!,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
} else if (famTuple) {
|
|
1308
|
+
const matched = resolveFamilyInstruments(
|
|
1309
|
+
famTuple,
|
|
1310
|
+
id,
|
|
1311
|
+
rawEntries.get("family")!,
|
|
1312
|
+
);
|
|
1313
|
+
instrumentVal = matched.length === 1 ? matched[0] : matched;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const result: Record<string, unknown> = {};
|
|
1317
|
+
if (famTuple) {
|
|
1318
|
+
result.family = famTuple;
|
|
1319
|
+
}
|
|
1320
|
+
if (instrumentVal !== undefined) {
|
|
1321
|
+
result.instrument = instrumentVal;
|
|
1322
|
+
}
|
|
1323
|
+
for (const [k, v] of rawEntries) {
|
|
1324
|
+
if (k === "family" || k === "instrument") continue;
|
|
1325
|
+
result[k] = data(v);
|
|
1326
|
+
}
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
642
1330
|
const result = Object.fromEntries(
|
|
643
|
-
[...
|
|
1331
|
+
[...rawEntries].map(([key, value]) => [key, data(value)]),
|
|
644
1332
|
);
|
|
645
1333
|
const selection = result.selection as
|
|
646
1334
|
| { instrument?: unknown }
|
|
@@ -692,66 +1380,86 @@ export function compile(
|
|
|
692
1380
|
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
693
1381
|
const constant =
|
|
694
1382
|
row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const f: Record<string, unknown> = {
|
|
698
|
-
name: row.key,
|
|
699
|
-
type,
|
|
700
|
-
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
701
|
-
};
|
|
702
|
-
if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
|
|
703
|
-
if (type === "text" && t.kind === "call") {
|
|
704
|
-
if (t.args.length < 2 || t.args.length > 3)
|
|
705
|
-
fail(
|
|
706
|
-
t,
|
|
707
|
-
"bounded text needs length bounds and an optional pattern",
|
|
708
|
-
"write text(1, 80)",
|
|
709
|
-
);
|
|
710
|
-
f.minLength = literal(resolve(t.args[0]!));
|
|
711
|
-
f.maxLength = literal(resolve(t.args[1]!));
|
|
712
|
-
if (t.args[2]) f.pattern = literal(resolve(t.args[2]));
|
|
713
|
-
}
|
|
714
|
-
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
715
|
-
if (t.args.length !== 2)
|
|
716
|
-
fail(
|
|
717
|
-
t,
|
|
718
|
-
"bounded fields need a minimum and maximum",
|
|
719
|
-
"write integer(1, 12) or money(0 SAR, 100 SAR)",
|
|
720
|
-
);
|
|
721
|
-
f.minimum = literal(resolve(t.args[0]!));
|
|
722
|
-
f.maximum = literal(resolve(t.args[1]!));
|
|
723
|
-
}
|
|
1383
|
+
const f = lowerFieldShape(row, resolve);
|
|
1384
|
+
const type = f.type;
|
|
724
1385
|
if (type === "list" && t.kind === "call") {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1386
|
+
const item = t.args[0]!;
|
|
1387
|
+
if (item.kind === "type" && item.name === "ref" && item.target) {
|
|
1388
|
+
f.target = text(
|
|
1389
|
+
resolve({ kind: "name", value: item.target, span: item.span }),
|
|
1390
|
+
).replaceAll(".", "_");
|
|
1391
|
+
f.targetKind = objects.has(String(f.target))
|
|
1392
|
+
? "object"
|
|
1393
|
+
: "instrument";
|
|
1394
|
+
}
|
|
731
1395
|
}
|
|
732
1396
|
if (type === "ref") {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
?
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1397
|
+
if (t.kind === "block") {
|
|
1398
|
+
const b = entries(t);
|
|
1399
|
+
const famNode = b.get("targetFamily") ?? b.get("family");
|
|
1400
|
+
let targetFamTuple: UdlFamily | undefined;
|
|
1401
|
+
if (famNode) {
|
|
1402
|
+
const famStr =
|
|
1403
|
+
famNode.kind === "name" ? famNode.value : text(famNode);
|
|
1404
|
+
targetFamTuple = resolveFamily(famStr, famNode);
|
|
1405
|
+
f.targetFamily = targetFamTuple;
|
|
1406
|
+
}
|
|
1407
|
+
if (b.has("target") || b.has("instrument")) {
|
|
1408
|
+
const tgtExpr = (b.get("target") ?? b.get("instrument"))!;
|
|
1409
|
+
const resolvedTgt = resolve(tgtExpr);
|
|
1410
|
+
let tgtVal: string | string[];
|
|
1411
|
+
if (resolvedTgt.kind === "list") {
|
|
1412
|
+
const items = resolvedTgt.items.map((it) =>
|
|
1413
|
+
text(resolve(it)).replaceAll(".", "_"),
|
|
1414
|
+
);
|
|
1415
|
+
tgtVal = items.length === 1 ? items[0]! : items;
|
|
1416
|
+
} else {
|
|
1417
|
+
tgtVal = text(resolvedTgt).replaceAll(".", "_");
|
|
1418
|
+
}
|
|
1419
|
+
f.target = tgtVal;
|
|
1420
|
+
if (targetFamTuple) {
|
|
1421
|
+
checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
|
|
1422
|
+
}
|
|
1423
|
+
} else if (targetFamTuple) {
|
|
1424
|
+
const matched = resolveFamilyInstruments(
|
|
1425
|
+
targetFamTuple,
|
|
1426
|
+
id,
|
|
1427
|
+
famNode!,
|
|
1428
|
+
);
|
|
1429
|
+
f.target = matched.length === 1 ? matched[0] : matched;
|
|
1430
|
+
}
|
|
1431
|
+
f.targetKind =
|
|
1432
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1433
|
+
? "object"
|
|
1434
|
+
: "instrument";
|
|
1435
|
+
} else {
|
|
1436
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
1437
|
+
if (!target)
|
|
1438
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
1439
|
+
const [root, ...tail] = target.split(".");
|
|
1440
|
+
const resolved = environment.get(root!);
|
|
1441
|
+
const resolvedTargets =
|
|
1442
|
+
resolved?.kind === "list"
|
|
1443
|
+
? resolved.items
|
|
1444
|
+
: resolved
|
|
1445
|
+
? [resolved]
|
|
1446
|
+
: [];
|
|
1447
|
+
const targets = resolvedTargets.map((value) =>
|
|
1448
|
+
[text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
|
|
1449
|
+
);
|
|
1450
|
+
f.target =
|
|
1451
|
+
target === "self"
|
|
1452
|
+
? id
|
|
1453
|
+
: targets.length === 1
|
|
1454
|
+
? targets[0]
|
|
1455
|
+
: targets.length
|
|
1456
|
+
? targets
|
|
1457
|
+
: target;
|
|
1458
|
+
f.targetKind =
|
|
1459
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1460
|
+
? "object"
|
|
1461
|
+
: "instrument";
|
|
1462
|
+
}
|
|
755
1463
|
} else if (type === "account") {
|
|
756
1464
|
if (t.kind === "call") {
|
|
757
1465
|
if (t.args.length < 1 || t.args.length > 4)
|
|
@@ -764,7 +1472,13 @@ export function compile(
|
|
|
764
1472
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
765
1473
|
if (t.args[2]) {
|
|
766
1474
|
const mode = text(t.args[2]);
|
|
767
|
-
if (
|
|
1475
|
+
if (mode === "external")
|
|
1476
|
+
fail(
|
|
1477
|
+
t,
|
|
1478
|
+
"external account mode was removed",
|
|
1479
|
+
"use a reservation and instruction-bound evidence",
|
|
1480
|
+
);
|
|
1481
|
+
if (mode === "contra") f.contra = true;
|
|
768
1482
|
else f.key = mode;
|
|
769
1483
|
}
|
|
770
1484
|
if (t.args[3]) f.key = text(t.args[3]);
|
|
@@ -889,6 +1603,7 @@ export function compile(
|
|
|
889
1603
|
lifecycle.transitions = {};
|
|
890
1604
|
const inst: UdlInstrument = {
|
|
891
1605
|
id,
|
|
1606
|
+
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
892
1607
|
title: title(id),
|
|
893
1608
|
summary: body.has("summary")
|
|
894
1609
|
? String(data(body.get("summary")!))
|
|
@@ -921,9 +1636,9 @@ export function compile(
|
|
|
921
1636
|
// appear after the instrument that asks about its fields.
|
|
922
1637
|
const bound = resolve(binding!);
|
|
923
1638
|
const [root, ...children] = text(bound).split(".");
|
|
924
|
-
const
|
|
925
|
-
let target =
|
|
926
|
-
? templates.get(
|
|
1639
|
+
const assignment = assignments.get(root!);
|
|
1640
|
+
let target = assignment
|
|
1641
|
+
? templates.get(assignment.target)?.body
|
|
927
1642
|
: program.decls
|
|
928
1643
|
.filter(
|
|
929
1644
|
(decl): decl is InstrumentDecl =>
|
|
@@ -971,6 +1686,221 @@ export function compile(
|
|
|
971
1686
|
}),
|
|
972
1687
|
});
|
|
973
1688
|
const slots = entries(selected(asBlock(row.value)));
|
|
1689
|
+
let actionSubject: UdlActionSubject | undefined;
|
|
1690
|
+
let subjectExpr = slots.get("subject");
|
|
1691
|
+
const boundaryBindings = new Set<string>();
|
|
1692
|
+
const authoredMoves = slots.get("moves");
|
|
1693
|
+
for (const move of authoredMoves?.kind === "list"
|
|
1694
|
+
? authoredMoves.items
|
|
1695
|
+
: authoredMoves
|
|
1696
|
+
? [authoredMoves]
|
|
1697
|
+
: []) {
|
|
1698
|
+
const parts = entries(asBlock(move));
|
|
1699
|
+
const boundary = parts.get("boundary");
|
|
1700
|
+
if (!boundary) continue;
|
|
1701
|
+
if (
|
|
1702
|
+
(parts.has("operation")
|
|
1703
|
+
? String(data(parts.get("operation")!))
|
|
1704
|
+
: "internal_transfer.create") !== "internal_transfer.reserve" ||
|
|
1705
|
+
parts.has("shares") ||
|
|
1706
|
+
parts.has("fee")
|
|
1707
|
+
)
|
|
1708
|
+
fail(
|
|
1709
|
+
move,
|
|
1710
|
+
"boundary dispatch requires a reservation",
|
|
1711
|
+
"reserve the exact amount before dispatch",
|
|
1712
|
+
);
|
|
1713
|
+
const adapterExpr = entries(asBlock(boundary)).get("adapter");
|
|
1714
|
+
if (!adapterExpr)
|
|
1715
|
+
fail(
|
|
1716
|
+
boundary,
|
|
1717
|
+
"boundary needs an adapter",
|
|
1718
|
+
"name a bound ADL adapter",
|
|
1719
|
+
);
|
|
1720
|
+
const binding = text(resolve(adapterExpr!));
|
|
1721
|
+
const target =
|
|
1722
|
+
options.adapterRegistry &&
|
|
1723
|
+
Object.hasOwn(options.adapterRegistry, binding)
|
|
1724
|
+
? options.adapterRegistry[binding]
|
|
1725
|
+
: undefined;
|
|
1726
|
+
if (
|
|
1727
|
+
!target ||
|
|
1728
|
+
!Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
1729
|
+
)
|
|
1730
|
+
fail(
|
|
1731
|
+
boundary,
|
|
1732
|
+
`unknown boundary adapter ${binding}`,
|
|
1733
|
+
"bind the named ADL adapter before compilation",
|
|
1734
|
+
);
|
|
1735
|
+
boundaryBindings.add(binding);
|
|
1736
|
+
}
|
|
1737
|
+
if (boundaryBindings.size) {
|
|
1738
|
+
const block = asBlock(subjectExpr);
|
|
1739
|
+
const declared = new Set(
|
|
1740
|
+
block.entries
|
|
1741
|
+
.filter((entry) => entry.key === "adapter")
|
|
1742
|
+
.flatMap((entry) =>
|
|
1743
|
+
entry.value.kind === "list"
|
|
1744
|
+
? entry.value.items.map(text)
|
|
1745
|
+
: [text(entry.value)],
|
|
1746
|
+
),
|
|
1747
|
+
);
|
|
1748
|
+
subjectExpr = {
|
|
1749
|
+
...block,
|
|
1750
|
+
entries: [
|
|
1751
|
+
...block.entries,
|
|
1752
|
+
...[...boundaryBindings]
|
|
1753
|
+
.filter((binding) => !declared.has(binding))
|
|
1754
|
+
.map((binding) => ({
|
|
1755
|
+
key: "adapter",
|
|
1756
|
+
value: {
|
|
1757
|
+
kind: "name" as const,
|
|
1758
|
+
value: binding,
|
|
1759
|
+
span: row.span,
|
|
1760
|
+
},
|
|
1761
|
+
span: row.span,
|
|
1762
|
+
})),
|
|
1763
|
+
],
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
if (subjectExpr) {
|
|
1767
|
+
const subjectBlock = asBlock(subjectExpr);
|
|
1768
|
+
const directRequirements: UdlSubjectRequirement[] = [];
|
|
1769
|
+
const adapterList: UdlActionSubject["adapters"] = [];
|
|
1770
|
+
for (const entry of subjectBlock.entries) {
|
|
1771
|
+
if (entry.key === "adapter") {
|
|
1772
|
+
const bindingNames =
|
|
1773
|
+
entry.value.kind === "list"
|
|
1774
|
+
? entry.value.items.map(text)
|
|
1775
|
+
: [text(entry.value)];
|
|
1776
|
+
for (const bindingName of bindingNames) {
|
|
1777
|
+
const target = options.adapterRegistry?.[bindingName];
|
|
1778
|
+
if (target) {
|
|
1779
|
+
const { adapter, operation } = target;
|
|
1780
|
+
const opBinding = adapter.operationMap[operation];
|
|
1781
|
+
if (
|
|
1782
|
+
opBinding &&
|
|
1783
|
+
opBinding.subjectRequirements !== undefined
|
|
1784
|
+
) {
|
|
1785
|
+
const validatedRequirements: UdlObjectField[] = [];
|
|
1786
|
+
for (const req of opBinding.subjectRequirements) {
|
|
1787
|
+
const result = udlObjectFieldSchema.safeParse(req);
|
|
1788
|
+
if (!result.success || result.data.optional) {
|
|
1789
|
+
fail(
|
|
1790
|
+
entry,
|
|
1791
|
+
`adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`,
|
|
1792
|
+
"ensure adapter subject requirements conform to UDL schema",
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
validatedRequirements.push(result.data);
|
|
1796
|
+
}
|
|
1797
|
+
const declaration = {
|
|
1798
|
+
provider: adapter.provider,
|
|
1799
|
+
capability: adapter.capability,
|
|
1800
|
+
operation,
|
|
1801
|
+
requirements: validatedRequirements,
|
|
1802
|
+
};
|
|
1803
|
+
const digest = sha256(
|
|
1804
|
+
new TextEncoder().encode(canonicalJson(declaration)),
|
|
1805
|
+
);
|
|
1806
|
+
const snapshot: UdlAdapterSubjectSnapshot = {
|
|
1807
|
+
...declaration,
|
|
1808
|
+
declarationDigest: Array.from(digest, (byte) =>
|
|
1809
|
+
byte.toString(16).padStart(2, "0"),
|
|
1810
|
+
).join(""),
|
|
1811
|
+
};
|
|
1812
|
+
const adapterRenames: Record<string, string> = {};
|
|
1813
|
+
if (attachmentInfo?.renames) {
|
|
1814
|
+
for (const req of snapshot.requirements) {
|
|
1815
|
+
if (attachmentInfo.renames.has(req.name)) {
|
|
1816
|
+
adapterRenames[req.name] = attachmentInfo.renames.get(
|
|
1817
|
+
req.name,
|
|
1818
|
+
)!;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
adapterList.push({
|
|
1823
|
+
binding: bindingName,
|
|
1824
|
+
snapshot,
|
|
1825
|
+
...(Object.keys(adapterRenames).length > 0
|
|
1826
|
+
? { renames: adapterRenames }
|
|
1827
|
+
: {}),
|
|
1828
|
+
});
|
|
1829
|
+
for (const reqField of snapshot.requirements) {
|
|
1830
|
+
const objectField = attachmentInfo?.renames.get(
|
|
1831
|
+
reqField.name,
|
|
1832
|
+
);
|
|
1833
|
+
const targetName = objectField ?? reqField.name;
|
|
1834
|
+
const existing = directRequirements.find(
|
|
1835
|
+
(r) => (r.objectField ?? r.field.name) === targetName,
|
|
1836
|
+
);
|
|
1837
|
+
if (existing) {
|
|
1838
|
+
if (!sameObjectField(existing.field, reqField)) {
|
|
1839
|
+
failWithCode(
|
|
1840
|
+
entry,
|
|
1841
|
+
"subject_field_conflict",
|
|
1842
|
+
`conflicting requirement ${reqField.name} in action ${name}`,
|
|
1843
|
+
"rename or unify the requirement",
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
} else {
|
|
1847
|
+
directRequirements.push({
|
|
1848
|
+
field: { ...reqField },
|
|
1849
|
+
...(objectField ? { objectField } : {}),
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
} else {
|
|
1854
|
+
adapterList.push({
|
|
1855
|
+
binding: bindingName,
|
|
1856
|
+
snapshot: null,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
} else {
|
|
1860
|
+
adapterList.push({
|
|
1861
|
+
binding: bindingName,
|
|
1862
|
+
snapshot: null,
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
} else {
|
|
1867
|
+
const fieldDef = lowerObjectField(entry, resolve);
|
|
1868
|
+
if (fieldDef.optional) {
|
|
1869
|
+
fail(
|
|
1870
|
+
entry,
|
|
1871
|
+
`subject requirement ${entry.key} cannot be optional`,
|
|
1872
|
+
"remove ? from requirement",
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const objectField = attachmentInfo?.renames.get(entry.key);
|
|
1876
|
+
const targetName = objectField ?? entry.key;
|
|
1877
|
+
const existing = directRequirements.find(
|
|
1878
|
+
(r) => (r.objectField ?? r.field.name) === targetName,
|
|
1879
|
+
);
|
|
1880
|
+
if (existing) {
|
|
1881
|
+
if (!sameObjectField(existing.field, fieldDef)) {
|
|
1882
|
+
failWithCode(
|
|
1883
|
+
entry,
|
|
1884
|
+
"subject_field_conflict",
|
|
1885
|
+
`conflicting requirement ${entry.key} in action ${name}`,
|
|
1886
|
+
"rename or unify the requirement",
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
} else {
|
|
1890
|
+
directRequirements.push({
|
|
1891
|
+
field: fieldDef,
|
|
1892
|
+
...(objectField ? { objectField } : {}),
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (directRequirements.length > 0 || adapterList.length > 0) {
|
|
1898
|
+
actionSubject = {
|
|
1899
|
+
requirements: directRequirements,
|
|
1900
|
+
adapters: adapterList,
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
974
1904
|
const a: UdlAction = {
|
|
975
1905
|
summary: slots.has("summary")
|
|
976
1906
|
? String(data(slots.get("summary")!))
|
|
@@ -981,7 +1911,20 @@ export function compile(
|
|
|
981
1911
|
input: lowerFields(asBlock(slots.get("input"))),
|
|
982
1912
|
requires: [],
|
|
983
1913
|
moves: [],
|
|
1914
|
+
...(actionSubject ? { subject: actionSubject } : {}),
|
|
984
1915
|
};
|
|
1916
|
+
for (const binding of boundaryBindings)
|
|
1917
|
+
if (
|
|
1918
|
+
!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1919
|
+
?.snapshot
|
|
1920
|
+
)
|
|
1921
|
+
fail(
|
|
1922
|
+
row,
|
|
1923
|
+
`boundary adapter ${binding} has no declared requirements`,
|
|
1924
|
+
"declare the adapter subject requirements, including an explicit empty list",
|
|
1925
|
+
);
|
|
1926
|
+
currentAction = a;
|
|
1927
|
+
currentActionName = name;
|
|
985
1928
|
if (name !== "create") {
|
|
986
1929
|
const from = slots.get("from");
|
|
987
1930
|
const to = slots.get("to");
|
|
@@ -999,7 +1942,8 @@ export function compile(
|
|
|
999
1942
|
};
|
|
1000
1943
|
}
|
|
1001
1944
|
for (const [key, expr] of slots) {
|
|
1002
|
-
if (["from", "to", "input", "summary"].includes(key))
|
|
1945
|
+
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
1946
|
+
continue;
|
|
1003
1947
|
if (key === "moves") {
|
|
1004
1948
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
1005
1949
|
for (const [index, move] of moves.entries()) {
|
|
@@ -1033,6 +1977,25 @@ export function compile(
|
|
|
1033
1977
|
const amount = parts.get("amount"),
|
|
1034
1978
|
from = parts.get("from"),
|
|
1035
1979
|
to = parts.get("to");
|
|
1980
|
+
if (amount) {
|
|
1981
|
+
const resolvedAmount = resolve(amount);
|
|
1982
|
+
if (
|
|
1983
|
+
resolvedAmount.kind === "name" &&
|
|
1984
|
+
resolvedAmount.value.startsWith("subject.")
|
|
1985
|
+
) {
|
|
1986
|
+
const subField = resolvedAmount.value.split(".")[1]!;
|
|
1987
|
+
const req = a.subject?.requirements.find(
|
|
1988
|
+
(r) => r.field.name === subField,
|
|
1989
|
+
);
|
|
1990
|
+
if (req && req.field.type !== "money") {
|
|
1991
|
+
fail(
|
|
1992
|
+
amount,
|
|
1993
|
+
`move amount subject.${subField} must have type money`,
|
|
1994
|
+
"use a money field",
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1036
1999
|
if (parts.has("shares")) {
|
|
1037
2000
|
if (
|
|
1038
2001
|
!amount ||
|
|
@@ -1064,7 +2027,7 @@ export function compile(
|
|
|
1064
2027
|
});
|
|
1065
2028
|
if (
|
|
1066
2029
|
recipient.kind !== "name" ||
|
|
1067
|
-
!
|
|
2030
|
+
!isParty(recipient.value) ||
|
|
1068
2031
|
rate.kind !== "percent"
|
|
1069
2032
|
)
|
|
1070
2033
|
fail(
|
|
@@ -1139,6 +2102,19 @@ export function compile(
|
|
|
1139
2102
|
...transfer,
|
|
1140
2103
|
operation: op,
|
|
1141
2104
|
capture: String(data(parts.get("capture")!)),
|
|
2105
|
+
...(parts.has("boundary")
|
|
2106
|
+
? {
|
|
2107
|
+
boundary: {
|
|
2108
|
+
adapter: text(
|
|
2109
|
+
resolve(
|
|
2110
|
+
entries(
|
|
2111
|
+
asBlock(parts.get("boundary")),
|
|
2112
|
+
).get("adapter")!,
|
|
2113
|
+
),
|
|
2114
|
+
),
|
|
2115
|
+
},
|
|
2116
|
+
}
|
|
2117
|
+
: {}),
|
|
1142
2118
|
}
|
|
1143
2119
|
: { ...transfer, operation: op },
|
|
1144
2120
|
);
|
|
@@ -1298,7 +2274,41 @@ export function compile(
|
|
|
1298
2274
|
a.approval = approval;
|
|
1299
2275
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
1300
2276
|
}
|
|
2277
|
+
if (attachmentInfo) {
|
|
2278
|
+
if (attachmentInfo.exposed.has(name)) {
|
|
2279
|
+
a.publicAction = attachmentInfo.exposed.get(name)!;
|
|
2280
|
+
} else {
|
|
2281
|
+
delete a.publicAction;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
1301
2284
|
if (automatic(a.actor)) delete a.publicAction;
|
|
2285
|
+
const checkSubjectPaths = (obj: unknown, span: Span) => {
|
|
2286
|
+
if (typeof obj === "string") {
|
|
2287
|
+
if (obj.startsWith("subject.")) {
|
|
2288
|
+
const subField = obj.split(".")[1]!;
|
|
2289
|
+
const req = a.subject?.requirements.find(
|
|
2290
|
+
(r) => r.field.name === subField,
|
|
2291
|
+
);
|
|
2292
|
+
if (!req) {
|
|
2293
|
+
failWithCode(
|
|
2294
|
+
{ span },
|
|
2295
|
+
"subject_field_unknown",
|
|
2296
|
+
`subject.${subField} names no declared subject requirement in action ${name}`,
|
|
2297
|
+
`declare ${subField} in subject { ... }`,
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
} else if (Array.isArray(obj)) {
|
|
2302
|
+
for (const item of obj) checkSubjectPaths(item, span);
|
|
2303
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
2304
|
+
for (const val of Object.values(obj)) checkSubjectPaths(val, span);
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
checkSubjectPaths(a.requires, row.span);
|
|
2308
|
+
checkSubjectPaths(a.set, row.span);
|
|
2309
|
+
checkSubjectPaths(a.invoke, row.span);
|
|
2310
|
+
currentAction = undefined;
|
|
2311
|
+
currentActionName = undefined;
|
|
1302
2312
|
for (const requirement of a.requires ?? []) {
|
|
1303
2313
|
if (
|
|
1304
2314
|
requirement.kind !== "approval" ||
|
|
@@ -1308,19 +2318,39 @@ export function compile(
|
|
|
1308
2318
|
const action = requirement.action ?? name;
|
|
1309
2319
|
const key = `${id}_${action}_decision`;
|
|
1310
2320
|
const previous = implicitDecisions.get(key);
|
|
1311
|
-
if (
|
|
2321
|
+
if (
|
|
2322
|
+
previous &&
|
|
2323
|
+
(previous.party !== requirement.party ||
|
|
2324
|
+
previous.protectedRequest !==
|
|
2325
|
+
(requirement.protectedRequest ?? "self"))
|
|
2326
|
+
)
|
|
1312
2327
|
fail(
|
|
1313
2328
|
{ span: origin },
|
|
1314
|
-
`action ${action} has
|
|
2329
|
+
`action ${action} has conflicting approval parties or protected requests`,
|
|
1315
2330
|
"use a separate decision action for each party",
|
|
1316
2331
|
);
|
|
1317
2332
|
implicitDecisions.set(key, {
|
|
1318
2333
|
target: id,
|
|
1319
2334
|
action,
|
|
1320
2335
|
party: requirement.party,
|
|
2336
|
+
protectedRequest: requirement.protectedRequest ?? "self",
|
|
1321
2337
|
origin,
|
|
1322
2338
|
});
|
|
1323
2339
|
}
|
|
2340
|
+
for (const requirement of a.subject?.requirements ?? []) {
|
|
2341
|
+
const entry =
|
|
2342
|
+
asBlock(subjectExpr).entries.find(
|
|
2343
|
+
(entry) => entry.key === requirement.field.name,
|
|
2344
|
+
) ??
|
|
2345
|
+
asBlock(subjectExpr).entries.find(
|
|
2346
|
+
(entry) => entry.key === "adapter",
|
|
2347
|
+
);
|
|
2348
|
+
requirementOrigins.set(requirement, {
|
|
2349
|
+
source: declarationSources.get(decl) ?? "program",
|
|
2350
|
+
span: entry?.span ?? row.span,
|
|
2351
|
+
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
1324
2354
|
inst.actions[name] = a;
|
|
1325
2355
|
inst.actionOrder.push(name);
|
|
1326
2356
|
}
|
|
@@ -1333,6 +2363,13 @@ export function compile(
|
|
|
1333
2363
|
path: `$.instruments[${document.instruments.length}]`,
|
|
1334
2364
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
1335
2365
|
});
|
|
2366
|
+
if (familyDeclaration && familyDeclaration.revision !== undefined) {
|
|
2367
|
+
inst.family = {
|
|
2368
|
+
module: familyDeclaration.module,
|
|
2369
|
+
exportPath: familyDeclaration.exportPath,
|
|
2370
|
+
revision: familyDeclaration.revision,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
1336
2373
|
document.instruments.push(inst);
|
|
1337
2374
|
for (const [key, definition] of records) {
|
|
1338
2375
|
const child: InstrumentDecl = {
|
|
@@ -1342,6 +2379,19 @@ export function compile(
|
|
|
1342
2379
|
body: asBlock(definition),
|
|
1343
2380
|
span: origin,
|
|
1344
2381
|
};
|
|
2382
|
+
declarationSources.set(
|
|
2383
|
+
child,
|
|
2384
|
+
declarationSources.get(decl) ?? "program",
|
|
2385
|
+
);
|
|
2386
|
+
const childFamily = familyDeclaration
|
|
2387
|
+
? {
|
|
2388
|
+
module: familyDeclaration.module,
|
|
2389
|
+
exportPath: `${familyDeclaration.exportPath}.${key}`,
|
|
2390
|
+
...(familyDeclaration.revision !== undefined
|
|
2391
|
+
? { revision: familyDeclaration.revision }
|
|
2392
|
+
: {}),
|
|
2393
|
+
}
|
|
2394
|
+
: undefined;
|
|
1345
2395
|
addInstrument(
|
|
1346
2396
|
child,
|
|
1347
2397
|
`${id}_${key}`,
|
|
@@ -1353,9 +2403,221 @@ export function compile(
|
|
|
1353
2403
|
]),
|
|
1354
2404
|
approvers,
|
|
1355
2405
|
enums,
|
|
2406
|
+
attachmentInfo
|
|
2407
|
+
? { ...attachmentInfo, exposed: new Map() }
|
|
2408
|
+
: undefined,
|
|
2409
|
+
childFamily,
|
|
1356
2410
|
);
|
|
1357
2411
|
}
|
|
1358
2412
|
};
|
|
2413
|
+
const compileObject = (decl: ObjectDecl) => {
|
|
2414
|
+
const body = entries(decl.body);
|
|
2415
|
+
for (const key of body.keys()) {
|
|
2416
|
+
if (
|
|
2417
|
+
key !== "fields" &&
|
|
2418
|
+
key !== "columns" &&
|
|
2419
|
+
!key.startsWith("attach ")
|
|
2420
|
+
) {
|
|
2421
|
+
fail(
|
|
2422
|
+
decl,
|
|
2423
|
+
`unknown object clause ${key}`,
|
|
2424
|
+
"use fields, columns, or attach",
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// 1. Lower authored fields
|
|
2430
|
+
const authoredFields: UdlObjectField[] = [];
|
|
2431
|
+
const authoredNames: string[] = [];
|
|
2432
|
+
const fieldsBlock = asBlock(body.get("fields"));
|
|
2433
|
+
for (const row of fieldsBlock.entries) {
|
|
2434
|
+
if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
|
|
2435
|
+
fail(
|
|
2436
|
+
row,
|
|
2437
|
+
`${row.key} is a reserved object name`,
|
|
2438
|
+
"rename this field",
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
const fieldDef = lowerObjectField(row);
|
|
2442
|
+
authoredFields.push(fieldDef);
|
|
2443
|
+
authoredNames.push(row.key);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// 2. Process attachments
|
|
2447
|
+
const attachments: UdlObjectAttachment[] = [];
|
|
2448
|
+
for (const entry of decl.body.entries) {
|
|
2449
|
+
if (!entry.key.startsWith("attach ")) continue;
|
|
2450
|
+
const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
|
|
2451
|
+
if (!match) {
|
|
2452
|
+
fail(
|
|
2453
|
+
entry,
|
|
2454
|
+
"invalid attach syntax",
|
|
2455
|
+
"write attach name = template { ... }",
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
const attachmentName = match[1]!;
|
|
2459
|
+
const targetTemplate = match[2]!;
|
|
2460
|
+
const template = templates.get(targetTemplate);
|
|
2461
|
+
if (!template) {
|
|
2462
|
+
fail(
|
|
2463
|
+
entry,
|
|
2464
|
+
`unknown instrument ${targetTemplate}`,
|
|
2465
|
+
`add use ${targetTemplate.split(".")[0]} and choose a declared instrument`,
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
const instId = `${decl.name}_${attachmentName}`;
|
|
2469
|
+
|
|
2470
|
+
const attachmentBlock = asBlock(entry.value);
|
|
2471
|
+
const renames = new Map<string, string>();
|
|
2472
|
+
const renameEntries = new Map<string, Entry>();
|
|
2473
|
+
const exposed = new Map<string, string>();
|
|
2474
|
+
const tunableEntries: Entry[] = [];
|
|
2475
|
+
|
|
2476
|
+
for (const row of attachmentBlock.entries) {
|
|
2477
|
+
if (row.key === "rename") {
|
|
2478
|
+
for (const r of asBlock(row.value).entries) {
|
|
2479
|
+
renames.set(r.key, text(r.value));
|
|
2480
|
+
renameEntries.set(r.key, r);
|
|
2481
|
+
}
|
|
2482
|
+
} else if (row.key === "expose") {
|
|
2483
|
+
if (row.value.kind === "call") {
|
|
2484
|
+
const actionName = row.value.name;
|
|
2485
|
+
const publicName = text(row.value.args[0]!);
|
|
2486
|
+
exposed.set(actionName, publicName);
|
|
2487
|
+
}
|
|
2488
|
+
} else {
|
|
2489
|
+
tunableEntries.push(row);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
const parties: Record<string, AttachmentPartyBinding> = {};
|
|
2494
|
+
attachments.push({ name: attachmentName, instrument: instId, parties });
|
|
2495
|
+
|
|
2496
|
+
const tunableBlock: BlockExpr = {
|
|
2497
|
+
kind: "block",
|
|
2498
|
+
entries: tunableEntries,
|
|
2499
|
+
span: entry.value.span,
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2502
|
+
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
2503
|
+
addInstrument(
|
|
2504
|
+
template,
|
|
2505
|
+
instId,
|
|
2506
|
+
tunableBlock,
|
|
2507
|
+
entry.span,
|
|
2508
|
+
new Map(),
|
|
2509
|
+
new Set(),
|
|
2510
|
+
new Map(),
|
|
2511
|
+
{
|
|
2512
|
+
subjectKindId: decl.name,
|
|
2513
|
+
attachmentName,
|
|
2514
|
+
renames,
|
|
2515
|
+
exposed,
|
|
2516
|
+
parties,
|
|
2517
|
+
},
|
|
2518
|
+
templateFamily ? { ...templateFamily } : undefined,
|
|
2519
|
+
);
|
|
2520
|
+
|
|
2521
|
+
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
2522
|
+
if (attachedInst?.actions.create) {
|
|
2523
|
+
const owned = new Set(
|
|
2524
|
+
attachedInst.calculate.map((node) => node.target),
|
|
2525
|
+
);
|
|
2526
|
+
for (const action of Object.values(attachedInst.actions)) {
|
|
2527
|
+
for (const node of action.calculate ?? []) owned.add(node.target);
|
|
2528
|
+
for (const move of action.moves)
|
|
2529
|
+
if ("capture" in move && move.capture) owned.add(move.capture);
|
|
2530
|
+
}
|
|
2531
|
+
const create = attachedInst.actions.create;
|
|
2532
|
+
for (const field of attachedInst.fields) {
|
|
2533
|
+
if (
|
|
2534
|
+
field.type === "account" ||
|
|
2535
|
+
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
2536
|
+
(field.type === "list" &&
|
|
2537
|
+
field.item === "ref" &&
|
|
2538
|
+
field.targetKind === "instrument") ||
|
|
2539
|
+
field.optional ||
|
|
2540
|
+
"value" in field ||
|
|
2541
|
+
owned.has(field.name)
|
|
2542
|
+
)
|
|
2543
|
+
continue;
|
|
2544
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
2545
|
+
const existing = create.subject.requirements.find(
|
|
2546
|
+
(item) => item.field.name === field.name,
|
|
2547
|
+
);
|
|
2548
|
+
if (existing) {
|
|
2549
|
+
if (canonicalJson(existing.field) !== canonicalJson(field))
|
|
2550
|
+
failWithCode(
|
|
2551
|
+
entry,
|
|
2552
|
+
"subject_field_conflict",
|
|
2553
|
+
`${instId}.create.subject.${field.name} conflicts with its instrument field`,
|
|
2554
|
+
"use the instrument field's type and constraints",
|
|
2555
|
+
);
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const objectField = renames.get(field.name);
|
|
2559
|
+
const requirement = {
|
|
2560
|
+
field,
|
|
2561
|
+
...(objectField ? { objectField } : {}),
|
|
2562
|
+
};
|
|
2563
|
+
create.subject.requirements.push(requirement);
|
|
2564
|
+
requirementOrigins.set(requirement, {
|
|
2565
|
+
source: declarationSources.get(template) ?? "program",
|
|
2566
|
+
span: entry.span,
|
|
2567
|
+
message: `${instId}.create.fields.${field.name}`,
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
for (const [oldName] of renames) {
|
|
2572
|
+
const found =
|
|
2573
|
+
attachedInst &&
|
|
2574
|
+
Object.values(attachedInst.actions).some((action) =>
|
|
2575
|
+
action.subject?.requirements.some(
|
|
2576
|
+
(requirement) => requirement.field.name === oldName,
|
|
2577
|
+
),
|
|
2578
|
+
);
|
|
2579
|
+
if (!found) {
|
|
2580
|
+
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
2581
|
+
failWithCode(
|
|
2582
|
+
renameEntry,
|
|
2583
|
+
"subject_field_unknown",
|
|
2584
|
+
`rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`,
|
|
2585
|
+
"rename a declared subject requirement",
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// 4. Validate columns
|
|
2592
|
+
const columnsExpr = body.get("columns");
|
|
2593
|
+
let columns: string[] = [];
|
|
2594
|
+
if (columnsExpr) {
|
|
2595
|
+
if (columnsExpr.kind !== "list") {
|
|
2596
|
+
fail(
|
|
2597
|
+
columnsExpr,
|
|
2598
|
+
"columns needs a list of field names",
|
|
2599
|
+
"write columns: [name, ...]",
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
columns = columnsExpr.items.map(text);
|
|
2603
|
+
if (columns.length > 8) {
|
|
2604
|
+
fail(
|
|
2605
|
+
columnsExpr,
|
|
2606
|
+
"at most 8 columns allowed",
|
|
2607
|
+
"choose up to 8 columns",
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
document.objects.push({
|
|
2613
|
+
id: decl.name,
|
|
2614
|
+
title: decl.title,
|
|
2615
|
+
authoredFields: authoredNames,
|
|
2616
|
+
attachments,
|
|
2617
|
+
fields: authoredFields,
|
|
2618
|
+
columns,
|
|
2619
|
+
});
|
|
2620
|
+
};
|
|
1359
2621
|
for (const decl of program.decls) {
|
|
1360
2622
|
if (decl.kind === "instrument") {
|
|
1361
2623
|
if (decl.parameters.length)
|
|
@@ -1366,15 +2628,172 @@ export function compile(
|
|
|
1366
2628
|
);
|
|
1367
2629
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
1368
2630
|
}
|
|
1369
|
-
if (decl.kind === "
|
|
1370
|
-
const template = templates.get(decl.
|
|
2631
|
+
if (decl.kind === "assignment") {
|
|
2632
|
+
const template = templates.get(decl.target);
|
|
1371
2633
|
if (!template)
|
|
1372
2634
|
fail(
|
|
1373
2635
|
decl,
|
|
1374
|
-
`unknown
|
|
1375
|
-
`add use ${decl.
|
|
2636
|
+
`unknown instrument ${decl.target}`,
|
|
2637
|
+
`add use ${decl.target.split(".")[0]} and choose a declared instrument`,
|
|
2638
|
+
);
|
|
2639
|
+
const templateFamily = resolveFamily(decl.target, decl, false);
|
|
2640
|
+
addInstrument(
|
|
2641
|
+
template,
|
|
2642
|
+
decl.name,
|
|
2643
|
+
decl.body,
|
|
2644
|
+
decl.span,
|
|
2645
|
+
new Map(),
|
|
2646
|
+
new Set(),
|
|
2647
|
+
new Map(),
|
|
2648
|
+
undefined,
|
|
2649
|
+
templateFamily ? { ...templateFamily } : undefined,
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
if (decl.kind === "object") {
|
|
2653
|
+
compileObject(decl);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
// Propagate mandatory invoked action requirements
|
|
2657
|
+
let changedInvocations = true;
|
|
2658
|
+
let invocationIterations = 0;
|
|
2659
|
+
while (changedInvocations && invocationIterations < 32) {
|
|
2660
|
+
changedInvocations = false;
|
|
2661
|
+
invocationIterations++;
|
|
2662
|
+
for (const inst of document.instruments) {
|
|
2663
|
+
for (const action of Object.values(inst.actions)) {
|
|
2664
|
+
for (const call of action.invoke ?? []) {
|
|
2665
|
+
if (call.guard) continue;
|
|
2666
|
+
let targetIds: string[] = [];
|
|
2667
|
+
if ("instrument" in call) {
|
|
2668
|
+
targetIds = [call.instrument];
|
|
2669
|
+
} else if ("selection" in call) {
|
|
2670
|
+
targetIds = Array.isArray(call.selection.instrument)
|
|
2671
|
+
? call.selection.instrument
|
|
2672
|
+
: [call.selection.instrument];
|
|
2673
|
+
} else if ("reference" in call) {
|
|
2674
|
+
const refField = resolveField(
|
|
2675
|
+
document,
|
|
2676
|
+
inst,
|
|
2677
|
+
call.reference,
|
|
2678
|
+
action.input,
|
|
2679
|
+
action,
|
|
2680
|
+
);
|
|
2681
|
+
if (
|
|
2682
|
+
refField?.type === "ref" &&
|
|
2683
|
+
refField.targetKind === "instrument"
|
|
2684
|
+
) {
|
|
2685
|
+
targetIds = Array.isArray(refField.target)
|
|
2686
|
+
? refField.target
|
|
2687
|
+
: [refField.target];
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
for (const targetId of targetIds) {
|
|
2691
|
+
const targetInst = document.instruments.find(
|
|
2692
|
+
(i) => i.id === targetId,
|
|
2693
|
+
);
|
|
2694
|
+
if (!targetInst) continue;
|
|
2695
|
+
if (
|
|
2696
|
+
inst.subject &&
|
|
2697
|
+
targetInst.subject &&
|
|
2698
|
+
inst.subject === targetInst.subject
|
|
2699
|
+
) {
|
|
2700
|
+
const targetAction = targetInst.actions[call.action];
|
|
2701
|
+
if (!targetAction?.subject) continue;
|
|
2702
|
+
if (!action.subject) {
|
|
2703
|
+
action.subject = { requirements: [], adapters: [] };
|
|
2704
|
+
}
|
|
2705
|
+
for (const adapter of targetAction.subject.adapters) {
|
|
2706
|
+
if (
|
|
2707
|
+
!action.subject.adapters.some(
|
|
2708
|
+
(existing) => existing.binding === adapter.binding,
|
|
2709
|
+
)
|
|
2710
|
+
) {
|
|
2711
|
+
action.subject.adapters.push(adapter);
|
|
2712
|
+
changedInvocations = true;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
for (const targetReq of targetAction.subject.requirements) {
|
|
2716
|
+
const targetObjFieldName =
|
|
2717
|
+
targetReq.objectField ?? targetReq.field.name;
|
|
2718
|
+
const existing = action.subject.requirements.find(
|
|
2719
|
+
(cr) =>
|
|
2720
|
+
(cr.objectField ?? cr.field.name) === targetObjFieldName,
|
|
2721
|
+
);
|
|
2722
|
+
if (!existing) {
|
|
2723
|
+
const inherited = {
|
|
2724
|
+
field: { ...targetReq.field, name: targetObjFieldName },
|
|
2725
|
+
};
|
|
2726
|
+
action.subject.requirements.push(inherited);
|
|
2727
|
+
requirementOrigins.set(
|
|
2728
|
+
inherited,
|
|
2729
|
+
requirementOrigins.get(targetReq)!,
|
|
2730
|
+
);
|
|
2731
|
+
changedInvocations = true;
|
|
2732
|
+
} else if (
|
|
2733
|
+
!sameObjectField(existing.field, targetReq.field)
|
|
2734
|
+
) {
|
|
2735
|
+
const first = requirementOrigins.get(existing)!;
|
|
2736
|
+
const second = requirementOrigins.get(targetReq)!;
|
|
2737
|
+
throw new CompileFailure({
|
|
2738
|
+
code: "subject_field_conflict",
|
|
2739
|
+
message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
|
|
2740
|
+
fix: "ensure compatible requirement definitions across invoked actions",
|
|
2741
|
+
span: first.span,
|
|
2742
|
+
source: first.source,
|
|
2743
|
+
related: [first, second],
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
for (const kind of document.objects) {
|
|
2754
|
+
const origins = new Map(
|
|
2755
|
+
kind.fields.map((field) => [
|
|
2756
|
+
field.name,
|
|
2757
|
+
`authored field ${kind.id}.${field.name}`,
|
|
2758
|
+
]),
|
|
2759
|
+
);
|
|
2760
|
+
for (const instrument of document.instruments.filter(
|
|
2761
|
+
(item) => item.subject === kind.id,
|
|
2762
|
+
)) {
|
|
2763
|
+
for (const name of instrument.actionOrder) {
|
|
2764
|
+
for (const requirement of instrument.actions[name]!.subject
|
|
2765
|
+
?.requirements ?? []) {
|
|
2766
|
+
const field = {
|
|
2767
|
+
...requirement.field,
|
|
2768
|
+
name: requirement.objectField ?? requirement.field.name,
|
|
2769
|
+
};
|
|
2770
|
+
const previous = kind.fields.find(
|
|
2771
|
+
(item) => item.name === field.name,
|
|
2772
|
+
);
|
|
2773
|
+
const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
|
|
2774
|
+
if (previous && !sameObjectField(previous, field)) {
|
|
2775
|
+
failWithCode(
|
|
2776
|
+
objects.get(kind.id)!,
|
|
2777
|
+
"subject_field_conflict",
|
|
2778
|
+
`${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`,
|
|
2779
|
+
"rename fields with different meanings",
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
if (!previous) {
|
|
2783
|
+
kind.fields.push(field);
|
|
2784
|
+
origins.set(field.name, origin);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
for (const column of kind.columns) {
|
|
2790
|
+
if (!kind.fields.some((field) => field.name === column))
|
|
2791
|
+
failWithCode(
|
|
2792
|
+
objects.get(kind.id)!,
|
|
2793
|
+
"subject_field_unknown",
|
|
2794
|
+
`column ${column} is unknown on ${kind.id}`,
|
|
2795
|
+
"name an authored field or attached requirement",
|
|
1376
2796
|
);
|
|
1377
|
-
addInstrument(template, decl.name, decl.body, decl.span);
|
|
1378
2797
|
}
|
|
1379
2798
|
}
|
|
1380
2799
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
@@ -1472,12 +2891,33 @@ export function compile(
|
|
|
1472
2891
|
(action) =>
|
|
1473
2892
|
action.approval?.action === decision.action &&
|
|
1474
2893
|
action.approval.party === decision.party &&
|
|
1475
|
-
inst.fields.some(
|
|
1476
|
-
(
|
|
1477
|
-
field.type
|
|
1478
|
-
field.target
|
|
1479
|
-
`self.${field.name}`
|
|
1480
|
-
|
|
2894
|
+
inst.fields.some((field) => {
|
|
2895
|
+
if (
|
|
2896
|
+
field.type !== "ref" ||
|
|
2897
|
+
field.target !== decision.target ||
|
|
2898
|
+
`self.${field.name}` !== action.approval?.target
|
|
2899
|
+
)
|
|
2900
|
+
return false;
|
|
2901
|
+
const [root, name, ...tail] =
|
|
2902
|
+
decision.protectedRequest.split(".");
|
|
2903
|
+
const input = name && action.approval.input[name];
|
|
2904
|
+
const request =
|
|
2905
|
+
root === "self"
|
|
2906
|
+
? [action.approval.target, name, ...tail]
|
|
2907
|
+
.filter(Boolean)
|
|
2908
|
+
.join(".")
|
|
2909
|
+
: root === "input" && name
|
|
2910
|
+
? input && "field" in input
|
|
2911
|
+
? [input.field, ...tail].join(".")
|
|
2912
|
+
: materialApprovals.has(action)
|
|
2913
|
+
? [`self.material_${name}`, ...tail].join(".")
|
|
2914
|
+
: undefined
|
|
2915
|
+
: undefined;
|
|
2916
|
+
return (
|
|
2917
|
+
request !== undefined &&
|
|
2918
|
+
action.approval.protectedRequest === request
|
|
2919
|
+
);
|
|
2920
|
+
}),
|
|
1481
2921
|
),
|
|
1482
2922
|
);
|
|
1483
2923
|
if (existing) continue;
|
|
@@ -1487,6 +2927,16 @@ export function compile(
|
|
|
1487
2927
|
`implicit decision name ${id} is already used`,
|
|
1488
2928
|
"rename the conflicting object",
|
|
1489
2929
|
);
|
|
2930
|
+
const protectedRequest =
|
|
2931
|
+
decision.protectedRequest === "self"
|
|
2932
|
+
? "self.target"
|
|
2933
|
+
: decision.protectedRequest.startsWith("self.")
|
|
2934
|
+
? `self.target.${decision.protectedRequest.slice(5)}`
|
|
2935
|
+
: fail(
|
|
2936
|
+
{ span: decision.origin },
|
|
2937
|
+
"implicit approval needs a stored protected request",
|
|
2938
|
+
"declare an explicit decision for an input-based protected request",
|
|
2939
|
+
);
|
|
1490
2940
|
addInstrument(
|
|
1491
2941
|
template,
|
|
1492
2942
|
id,
|
|
@@ -1497,10 +2947,14 @@ export function compile(
|
|
|
1497
2947
|
for: decision.target,
|
|
1498
2948
|
approved_by: decision.party,
|
|
1499
2949
|
action: decision.action,
|
|
2950
|
+
protected_request: protectedRequest,
|
|
1500
2951
|
}).map(([key, value]) => ({
|
|
1501
2952
|
key,
|
|
1502
2953
|
value: {
|
|
1503
|
-
kind:
|
|
2954
|
+
kind:
|
|
2955
|
+
key === "action" || key === "protected_request"
|
|
2956
|
+
? "text"
|
|
2957
|
+
: "name",
|
|
1504
2958
|
value,
|
|
1505
2959
|
span: decision.origin,
|
|
1506
2960
|
},
|
|
@@ -1508,6 +2962,22 @@ export function compile(
|
|
|
1508
2962
|
})),
|
|
1509
2963
|
},
|
|
1510
2964
|
decision.origin,
|
|
2965
|
+
new Map(),
|
|
2966
|
+
new Set(),
|
|
2967
|
+
new Map(),
|
|
2968
|
+
document.instruments.find(
|
|
2969
|
+
(instrument) => instrument.id === decision.target,
|
|
2970
|
+
)?.subject
|
|
2971
|
+
? {
|
|
2972
|
+
subjectKindId: document.instruments.find(
|
|
2973
|
+
(instrument) => instrument.id === decision.target,
|
|
2974
|
+
)!.subject!,
|
|
2975
|
+
attachmentName: id,
|
|
2976
|
+
parties: {},
|
|
2977
|
+
renames: new Map(),
|
|
2978
|
+
exposed: new Map(),
|
|
2979
|
+
}
|
|
2980
|
+
: undefined,
|
|
1511
2981
|
);
|
|
1512
2982
|
}
|
|
1513
2983
|
}
|
|
@@ -1578,7 +3048,7 @@ export function compile(
|
|
|
1578
3048
|
.find((o) => i.path.startsWith(o.path));
|
|
1579
3049
|
return diagnostic(
|
|
1580
3050
|
{
|
|
1581
|
-
code: "HSX1601",
|
|
3051
|
+
code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
|
|
1582
3052
|
message: `${i.path}: ${i.message}`,
|
|
1583
3053
|
fix: i.fix,
|
|
1584
3054
|
span: origin?.span ?? program.span,
|