@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/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,188 @@ 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 collectChildExportPaths = (
|
|
700
|
+
parentDecl: InstrumentDecl,
|
|
701
|
+
suffix: string,
|
|
702
|
+
): string[] => {
|
|
703
|
+
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
704
|
+
const matches: string[] = [];
|
|
705
|
+
for (const [recName] of recs) {
|
|
706
|
+
if (suffix === recName) matches.push(recName);
|
|
707
|
+
}
|
|
708
|
+
for (const [recName, recBlock] of recs) {
|
|
709
|
+
if (suffix.startsWith(`${recName}_`)) {
|
|
710
|
+
const nested = collectChildExportPaths(
|
|
711
|
+
{
|
|
712
|
+
kind: "instrument",
|
|
713
|
+
name: recName,
|
|
714
|
+
parameters: [],
|
|
715
|
+
body: asBlock(recBlock),
|
|
716
|
+
span: parentDecl.span,
|
|
717
|
+
},
|
|
718
|
+
suffix.slice(recName.length + 1),
|
|
719
|
+
);
|
|
720
|
+
for (const rest of nested) {
|
|
721
|
+
matches.push(`${recName}.${rest}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return matches;
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const resolveChildExportPath = (
|
|
729
|
+
parentDecl: InstrumentDecl,
|
|
730
|
+
suffix: string,
|
|
731
|
+
): string | undefined => {
|
|
732
|
+
const matches = collectChildExportPaths(parentDecl, suffix);
|
|
733
|
+
if (matches.length > 1) {
|
|
734
|
+
failWithCode(
|
|
735
|
+
parentDecl,
|
|
736
|
+
"HSX1001",
|
|
737
|
+
`ambiguous child export path suffix '${suffix}': multiple candidates (${matches.join(", ")})`,
|
|
738
|
+
"rename conflicting records to remove duplicate export path suffixes",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return matches[0];
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
|
|
745
|
+
const existing = document.instruments.find((i) => i.id === targetId);
|
|
746
|
+
if (existing?.family) return existing.family;
|
|
747
|
+
|
|
748
|
+
if (targetId.startsWith(`${id}_`) && familyDeclaration) {
|
|
749
|
+
const sub = resolveChildExportPath(
|
|
750
|
+
decl,
|
|
751
|
+
targetId.slice(id.length + 1),
|
|
752
|
+
);
|
|
753
|
+
if (sub) {
|
|
754
|
+
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
755
|
+
try {
|
|
756
|
+
return resolveFamily(
|
|
757
|
+
`${familyDeclaration.module}.${fullExport}`,
|
|
758
|
+
{ span: origin },
|
|
759
|
+
false,
|
|
760
|
+
);
|
|
761
|
+
} catch {}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
for (const [asgnName, asgn] of assignments) {
|
|
766
|
+
if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
|
|
767
|
+
const tmpl = templates.get(asgn.target);
|
|
768
|
+
if (!tmpl) continue;
|
|
769
|
+
const mod = declarationSources.get(tmpl);
|
|
770
|
+
if (!mod || mod === "program") continue;
|
|
771
|
+
if (targetId === asgnName) {
|
|
772
|
+
try {
|
|
773
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
774
|
+
} catch {}
|
|
775
|
+
} else {
|
|
776
|
+
const sub = resolveChildExportPath(
|
|
777
|
+
tmpl,
|
|
778
|
+
targetId.slice(asgnName.length + 1),
|
|
779
|
+
);
|
|
780
|
+
if (sub) {
|
|
781
|
+
const fullTarget = `${asgn.target}.${sub}`;
|
|
782
|
+
try {
|
|
783
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
784
|
+
} catch {}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return undefined;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const checkTargetFamily = (
|
|
793
|
+
targetIds: string | string[],
|
|
794
|
+
expectedFamily: UdlFamily,
|
|
795
|
+
expr: { span: Span; source?: string },
|
|
796
|
+
): void => {
|
|
797
|
+
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
798
|
+
for (const tid of ids) {
|
|
799
|
+
const fam = getInstrumentFamily(tid);
|
|
800
|
+
if (
|
|
801
|
+
!fam ||
|
|
802
|
+
fam.module !== expectedFamily.module ||
|
|
803
|
+
fam.exportPath !== expectedFamily.exportPath ||
|
|
804
|
+
fam.revision !== expectedFamily.revision
|
|
805
|
+
) {
|
|
806
|
+
failWithCode(
|
|
807
|
+
expr,
|
|
808
|
+
"HSX1001",
|
|
809
|
+
`target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`,
|
|
810
|
+
"ensure target instrument matches the declared family",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
|
|
328
816
|
const enums = new Map(inheritedEnums);
|
|
329
817
|
for (const parameter of decl.parameters) {
|
|
330
818
|
const type =
|
|
@@ -335,18 +823,44 @@ export function compile(
|
|
|
335
823
|
enums.set(parameter.key, type.args.map(text));
|
|
336
824
|
}
|
|
337
825
|
const approvers = new Set(inheritedApprovers);
|
|
338
|
-
const supplied =
|
|
826
|
+
const supplied = new Map<string, Expr>();
|
|
827
|
+
for (const entry of arguments_.entries) {
|
|
828
|
+
if (supplied.has(entry.key))
|
|
829
|
+
fail(
|
|
830
|
+
entry,
|
|
831
|
+
`duplicate tunable ${entry.key}`,
|
|
832
|
+
"supply each parameter once",
|
|
833
|
+
);
|
|
834
|
+
supplied.set(entry.key, entry.value);
|
|
835
|
+
}
|
|
339
836
|
const environment = new Map<string, Expr>(inherited);
|
|
340
837
|
for (const param of decl.parameters) {
|
|
341
838
|
const type =
|
|
342
839
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
343
840
|
const fallback =
|
|
344
841
|
param.value.kind === "default" ? param.value.value : undefined;
|
|
345
|
-
const
|
|
842
|
+
const typeName =
|
|
843
|
+
type.kind === "type" || type.kind === "call" ? type.name : text(type);
|
|
844
|
+
const partyParameter =
|
|
845
|
+
attachmentInfo && (typeName === "party" || typeName === "approval");
|
|
846
|
+
const byName: Expr | undefined =
|
|
847
|
+
partyParameter &&
|
|
848
|
+
(subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
|
|
849
|
+
document.parties[param.key])
|
|
850
|
+
? { kind: "name", value: param.key, span: origin }
|
|
851
|
+
: undefined;
|
|
852
|
+
const actual =
|
|
853
|
+
supplied.get(param.key) ??
|
|
854
|
+
byName ??
|
|
855
|
+
(fallback && {
|
|
856
|
+
...fallback,
|
|
857
|
+
source: declarationSources.get(decl) ?? "program",
|
|
858
|
+
});
|
|
346
859
|
if (!actual) {
|
|
347
860
|
if (type.kind === "type" && type.optional) continue;
|
|
348
|
-
|
|
861
|
+
failWithCode(
|
|
349
862
|
{ span: origin },
|
|
863
|
+
partyParameter ? "subject_party_unbound" : "HSX1001",
|
|
350
864
|
`${id} needs ${param.key}`,
|
|
351
865
|
`add ${param.key}: value inside ${id}`,
|
|
352
866
|
);
|
|
@@ -360,7 +874,16 @@ export function compile(
|
|
|
360
874
|
`unknown tunable ${key}`,
|
|
361
875
|
`choose ${decl.parameters.map((p) => p.key).join(", ")}`,
|
|
362
876
|
);
|
|
363
|
-
const
|
|
877
|
+
const isParty = (name: string) =>
|
|
878
|
+
!!document.parties[name] ||
|
|
879
|
+
(!!attachmentInfo &&
|
|
880
|
+
subjectPartyRoles.includes(name as SubjectPartyRole));
|
|
881
|
+
const resolvedParties = new Set<string>();
|
|
882
|
+
const resolve = (
|
|
883
|
+
expr: Expr,
|
|
884
|
+
seen = new Set<string>(),
|
|
885
|
+
partyBinding = false,
|
|
886
|
+
): Expr => {
|
|
364
887
|
if (
|
|
365
888
|
expr.kind === "call" &&
|
|
366
889
|
["object", "all", "party"].includes(expr.name)
|
|
@@ -375,22 +898,33 @@ export function compile(
|
|
|
375
898
|
const matches =
|
|
376
899
|
expr.name === "party"
|
|
377
900
|
? Object.entries(document.parties)
|
|
378
|
-
.filter(
|
|
901
|
+
.filter(
|
|
902
|
+
([name, party]) =>
|
|
903
|
+
party.kind === type && (!partyBinding || names.has(name)),
|
|
904
|
+
)
|
|
379
905
|
.map(([name]) => name)
|
|
380
|
-
: [...
|
|
906
|
+
: [...assignments.values()]
|
|
907
|
+
.filter(
|
|
908
|
+
(assignment) =>
|
|
909
|
+
expr.name === "all" ||
|
|
910
|
+
!attachmentSubjects.has(assignment.name) ||
|
|
911
|
+
attachmentSubjects.get(assignment.name) ===
|
|
912
|
+
attachmentInfo?.subjectKindId,
|
|
913
|
+
)
|
|
381
914
|
.filter(
|
|
382
|
-
(
|
|
383
|
-
|
|
384
|
-
type.startsWith(`${
|
|
915
|
+
(assignment) =>
|
|
916
|
+
assignment.target === type ||
|
|
917
|
+
type.startsWith(`${assignment.target}.`),
|
|
385
918
|
)
|
|
386
919
|
.map(
|
|
387
|
-
(
|
|
388
|
-
|
|
389
|
-
type.slice(
|
|
920
|
+
(assignment) =>
|
|
921
|
+
assignment.name +
|
|
922
|
+
type.slice(assignment.target.length).replaceAll(".", "_"),
|
|
390
923
|
);
|
|
391
924
|
if (expr.name !== "all" && matches.length !== 1)
|
|
392
|
-
|
|
393
|
-
|
|
925
|
+
failWithCode(
|
|
926
|
+
expr,
|
|
927
|
+
partyBinding ? "subject_party_unbound" : "HSX1001",
|
|
394
928
|
`${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
|
|
395
929
|
"declare the required object or supply this tunable explicitly",
|
|
396
930
|
);
|
|
@@ -404,25 +938,46 @@ export function compile(
|
|
|
404
938
|
: items[0]!;
|
|
405
939
|
}
|
|
406
940
|
if (expr.kind !== "name") return expr;
|
|
941
|
+
const binding = environment.get(expr.value);
|
|
942
|
+
if (
|
|
943
|
+
attachmentInfo &&
|
|
944
|
+
subjectPartyRoles.includes(expr.value as SubjectPartyRole) &&
|
|
945
|
+
(!binding ||
|
|
946
|
+
(binding.kind === "name" && binding.value === expr.value))
|
|
947
|
+
)
|
|
948
|
+
return expr;
|
|
949
|
+
if (attachmentInfo) {
|
|
950
|
+
const [local, ...tail] = expr.value.split(".");
|
|
951
|
+
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
952
|
+
if (attachmentSubjects.has(target))
|
|
953
|
+
return { ...expr, value: [target, ...tail].join("_") };
|
|
954
|
+
}
|
|
407
955
|
if (expr.value.startsWith("party.")) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
956
|
+
const [, party, ...members] = expr.value.split(".");
|
|
957
|
+
const binding = environment.get(party!);
|
|
958
|
+
if (binding?.kind === "name" && isParty(binding.value))
|
|
959
|
+
return {
|
|
960
|
+
...expr,
|
|
961
|
+
value: ["party", binding.value, ...members].join("."),
|
|
962
|
+
};
|
|
411
963
|
}
|
|
412
964
|
const [root, ...tail] = expr.value.split(".");
|
|
413
965
|
const bound = environment.get(root!);
|
|
414
966
|
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
415
967
|
return expr;
|
|
416
968
|
if (seen.has(root!))
|
|
417
|
-
return
|
|
969
|
+
return failWithCode(
|
|
418
970
|
expr,
|
|
971
|
+
partyBinding ? "subject_party_unbound" : "HSX1001",
|
|
419
972
|
`cyclic tunable ${root}`,
|
|
420
973
|
"replace the cycle with a literal or declared reference",
|
|
421
974
|
);
|
|
422
975
|
let resolved =
|
|
423
|
-
|
|
976
|
+
resolvedParties.has(root!) ||
|
|
977
|
+
(!partyBinding &&
|
|
978
|
+
(supplied.has(root!) || inherited.has(root!) || enums.has(root!)))
|
|
424
979
|
? bound
|
|
425
|
-
: resolve(bound, new Set([...seen, root!]));
|
|
980
|
+
: resolve(bound, new Set([...seen, root!]), partyBinding);
|
|
426
981
|
for (const key of tail) {
|
|
427
982
|
if (resolved.kind !== "block") return expr;
|
|
428
983
|
const child = entries(resolved).get(key);
|
|
@@ -432,7 +987,7 @@ export function compile(
|
|
|
432
987
|
`missing tunable ${expr.value}`,
|
|
433
988
|
`declare ${key} in ${root}`,
|
|
434
989
|
);
|
|
435
|
-
resolved = resolve(child, new Set([...seen, root!]));
|
|
990
|
+
resolved = resolve(child, new Set([...seen, root!]), partyBinding);
|
|
436
991
|
}
|
|
437
992
|
return resolved;
|
|
438
993
|
};
|
|
@@ -443,7 +998,13 @@ export function compile(
|
|
|
443
998
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
444
999
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
445
1000
|
const v =
|
|
446
|
-
supplied.has(param.key) || type === "enum"
|
|
1001
|
+
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1002
|
+
? actual
|
|
1003
|
+
: resolve(
|
|
1004
|
+
actual,
|
|
1005
|
+
new Set(),
|
|
1006
|
+
!!attachmentInfo && (type === "party" || type === "approval"),
|
|
1007
|
+
);
|
|
447
1008
|
environment.set(param.key, v);
|
|
448
1009
|
if (type === "enum" && t.kind === "call") {
|
|
449
1010
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -456,12 +1017,35 @@ export function compile(
|
|
|
456
1017
|
if (v.kind !== "list")
|
|
457
1018
|
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
458
1019
|
} else if (type === "party" || type === "approval") {
|
|
459
|
-
if (v.kind !== "name" || !
|
|
460
|
-
|
|
461
|
-
|
|
1020
|
+
if (v.kind !== "name" || !isParty(v.value))
|
|
1021
|
+
failWithCode(
|
|
1022
|
+
actual,
|
|
1023
|
+
attachmentInfo ? "subject_party_unbound" : "HSX1001",
|
|
462
1024
|
`${param.key} needs a declared party`,
|
|
463
1025
|
"declare a party and use its name here",
|
|
464
1026
|
);
|
|
1027
|
+
const party = document.parties[v.value];
|
|
1028
|
+
if (
|
|
1029
|
+
(type === "approval" && (party?.kind !== "staff" || !party.role)) ||
|
|
1030
|
+
(type === "party" &&
|
|
1031
|
+
(party?.kind === "staff" ||
|
|
1032
|
+
(attachmentInfo && party?.kind === "person")))
|
|
1033
|
+
)
|
|
1034
|
+
failWithCode(
|
|
1035
|
+
actual,
|
|
1036
|
+
"party_kind_mismatch",
|
|
1037
|
+
`${param.key} cannot bind ${v.value}`,
|
|
1038
|
+
type === "approval"
|
|
1039
|
+
? "use a declared staff party with a role"
|
|
1040
|
+
: "use a subject role or declared business",
|
|
1041
|
+
);
|
|
1042
|
+
resolvedParties.add(param.key);
|
|
1043
|
+
if (attachmentInfo)
|
|
1044
|
+
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
|
|
1045
|
+
v.value as SubjectPartyRole,
|
|
1046
|
+
)
|
|
1047
|
+
? { role: v.value as SubjectPartyRole }
|
|
1048
|
+
: { party: v.value };
|
|
465
1049
|
if (type === "approval") approvers.add(text(v));
|
|
466
1050
|
} else if (type === "ref") {
|
|
467
1051
|
const values = v.kind === "list" ? v.items : [v];
|
|
@@ -487,14 +1071,17 @@ export function compile(
|
|
|
487
1071
|
);
|
|
488
1072
|
const [root, ...tail] = value.value.split(".");
|
|
489
1073
|
const obj = objects.get(root!);
|
|
1074
|
+
const assignment = assignments.get(root!);
|
|
1075
|
+
const targetType = obj ? obj.name : assignment?.target;
|
|
490
1076
|
if (
|
|
491
1077
|
(!obj &&
|
|
1078
|
+
!assignment &&
|
|
492
1079
|
!document.instruments.some(
|
|
493
1080
|
(inst) => inst.id === value.value,
|
|
494
1081
|
)) ||
|
|
495
1082
|
(t.kind === "type" &&
|
|
496
1083
|
t.target &&
|
|
497
|
-
[
|
|
1084
|
+
[targetType, ...tail].join(".") !== t.target)
|
|
498
1085
|
)
|
|
499
1086
|
fail(
|
|
500
1087
|
value,
|
|
@@ -590,6 +1177,7 @@ export function compile(
|
|
|
590
1177
|
for (const key of body.keys())
|
|
591
1178
|
if (
|
|
592
1179
|
![
|
|
1180
|
+
"familyRevision",
|
|
593
1181
|
"fields",
|
|
594
1182
|
"lifecycle",
|
|
595
1183
|
"records",
|
|
@@ -606,6 +1194,24 @@ export function compile(
|
|
|
606
1194
|
`unknown instrument clause ${key}`,
|
|
607
1195
|
"use fields, lifecycle, actions, invariants, or records",
|
|
608
1196
|
);
|
|
1197
|
+
if (body.has("familyRevision")) {
|
|
1198
|
+
const revExpr = body.get("familyRevision")!;
|
|
1199
|
+
const revVal = literal(revExpr);
|
|
1200
|
+
if (
|
|
1201
|
+
typeof revVal !== "number" ||
|
|
1202
|
+
!Number.isInteger(revVal) ||
|
|
1203
|
+
revVal <= 0
|
|
1204
|
+
) {
|
|
1205
|
+
fail(
|
|
1206
|
+
revExpr,
|
|
1207
|
+
"familyRevision must be a positive integer",
|
|
1208
|
+
"use a positive integer revision",
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (familyDeclaration) {
|
|
1212
|
+
familyDeclaration.revision = revVal;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
609
1215
|
const records = entries(asBlock(body.get("records")));
|
|
610
1216
|
if (!inherited.size)
|
|
611
1217
|
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
@@ -618,11 +1224,63 @@ export function compile(
|
|
|
618
1224
|
});
|
|
619
1225
|
const fields: UdlField[] = [];
|
|
620
1226
|
const calculations: UdlCalculation[] = [];
|
|
1227
|
+
let currentAction: UdlAction | undefined;
|
|
1228
|
+
let currentActionName: string | undefined;
|
|
621
1229
|
const path = (expr: Expr): string => {
|
|
622
1230
|
const value = resolve(expr);
|
|
623
1231
|
const name = text(value);
|
|
624
|
-
if (
|
|
625
|
-
|
|
1232
|
+
if (isParty(name)) return `party.${name}`;
|
|
1233
|
+
if (name.startsWith("subject.")) {
|
|
1234
|
+
const subField = name.split(".")[1]!;
|
|
1235
|
+
if (currentAction) {
|
|
1236
|
+
const req = currentAction.subject?.requirements.find(
|
|
1237
|
+
(r) => r.field.name === subField,
|
|
1238
|
+
);
|
|
1239
|
+
if (!req) {
|
|
1240
|
+
failWithCode(
|
|
1241
|
+
expr,
|
|
1242
|
+
"subject_field_unknown",
|
|
1243
|
+
`subject.${subField} names no declared subject requirement in action ${currentActionName}`,
|
|
1244
|
+
`declare ${subField} in subject { ... }`,
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
const declaredInAction = decl.body.entries.some((e) => {
|
|
1249
|
+
if (!e.key.startsWith("action ")) return false;
|
|
1250
|
+
const subBlock = entries(asBlock(e.value)).get("subject");
|
|
1251
|
+
if (!subBlock) return false;
|
|
1252
|
+
return asBlock(subBlock).entries.some((se) => {
|
|
1253
|
+
if (se.key === subField) return true;
|
|
1254
|
+
if (se.key === "adapter") {
|
|
1255
|
+
const names =
|
|
1256
|
+
se.value.kind === "list"
|
|
1257
|
+
? se.value.items.map(text)
|
|
1258
|
+
: [text(se.value)];
|
|
1259
|
+
return names.some((n) => {
|
|
1260
|
+
const reg = options.adapterRegistry?.[n];
|
|
1261
|
+
if (!reg) return false;
|
|
1262
|
+
const op = reg.adapter.operationMap[reg.operation];
|
|
1263
|
+
return op?.subjectRequirements?.some(
|
|
1264
|
+
(sr) => sr.name === subField,
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
if (!declaredInAction) {
|
|
1272
|
+
failWithCode(
|
|
1273
|
+
expr,
|
|
1274
|
+
"subject_field_unknown",
|
|
1275
|
+
`subject.${subField} names no declared subject requirement in instrument ${id}`,
|
|
1276
|
+
`declare ${subField} in an action subject { ... }`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return /^(self|input|party|subject)\./.test(name)
|
|
1282
|
+
? name
|
|
1283
|
+
: `self.${name}`;
|
|
626
1284
|
};
|
|
627
1285
|
const val = (expr: Expr): UdlValue => {
|
|
628
1286
|
const v = resolve(expr);
|
|
@@ -639,8 +1297,57 @@ export function compile(
|
|
|
639
1297
|
return id;
|
|
640
1298
|
const value = resolve(expr);
|
|
641
1299
|
if (value.kind === "block") {
|
|
1300
|
+
const rawEntries = entries(value);
|
|
1301
|
+
if (
|
|
1302
|
+
(rawEntries.has("family") && !rawEntries.has("kind")) ||
|
|
1303
|
+
((rawEntries.has("states") ||
|
|
1304
|
+
rawEntries.has("reference") ||
|
|
1305
|
+
rawEntries.has("anchor")) &&
|
|
1306
|
+
rawEntries.has("instrument"))
|
|
1307
|
+
) {
|
|
1308
|
+
let famTuple: UdlFamily | undefined;
|
|
1309
|
+
if (rawEntries.has("family")) {
|
|
1310
|
+
const famExpr = rawEntries.get("family")!;
|
|
1311
|
+
const famStr =
|
|
1312
|
+
famExpr.kind === "name" ? famExpr.value : text(famExpr);
|
|
1313
|
+
famTuple = resolveFamily(famStr, famExpr);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
let instrumentVal: unknown;
|
|
1317
|
+
if (rawEntries.has("instrument")) {
|
|
1318
|
+
instrumentVal = data(rawEntries.get("instrument")!);
|
|
1319
|
+
if (famTuple) {
|
|
1320
|
+
checkTargetFamily(
|
|
1321
|
+
instrumentVal as string | string[],
|
|
1322
|
+
famTuple,
|
|
1323
|
+
rawEntries.get("instrument")!,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
} else if (famTuple) {
|
|
1327
|
+
const matched = resolveFamilyInstruments(
|
|
1328
|
+
famTuple,
|
|
1329
|
+
id,
|
|
1330
|
+
rawEntries.get("family")!,
|
|
1331
|
+
);
|
|
1332
|
+
instrumentVal = matched.length === 1 ? matched[0] : matched;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const result: Record<string, unknown> = {};
|
|
1336
|
+
if (famTuple) {
|
|
1337
|
+
result.family = famTuple;
|
|
1338
|
+
}
|
|
1339
|
+
if (instrumentVal !== undefined) {
|
|
1340
|
+
result.instrument = instrumentVal;
|
|
1341
|
+
}
|
|
1342
|
+
for (const [k, v] of rawEntries) {
|
|
1343
|
+
if (k === "family" || k === "instrument") continue;
|
|
1344
|
+
result[k] = data(v);
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
642
1349
|
const result = Object.fromEntries(
|
|
643
|
-
[...
|
|
1350
|
+
[...rawEntries].map(([key, value]) => [key, data(value)]),
|
|
644
1351
|
);
|
|
645
1352
|
const selection = result.selection as
|
|
646
1353
|
| { instrument?: unknown }
|
|
@@ -692,66 +1399,86 @@ export function compile(
|
|
|
692
1399
|
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
693
1400
|
const constant =
|
|
694
1401
|
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
|
-
}
|
|
1402
|
+
const f = lowerFieldShape(row, resolve);
|
|
1403
|
+
const type = f.type;
|
|
724
1404
|
if (type === "list" && t.kind === "call") {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1405
|
+
const item = t.args[0]!;
|
|
1406
|
+
if (item.kind === "type" && item.name === "ref" && item.target) {
|
|
1407
|
+
f.target = text(
|
|
1408
|
+
resolve({ kind: "name", value: item.target, span: item.span }),
|
|
1409
|
+
).replaceAll(".", "_");
|
|
1410
|
+
f.targetKind = objects.has(String(f.target))
|
|
1411
|
+
? "object"
|
|
1412
|
+
: "instrument";
|
|
1413
|
+
}
|
|
731
1414
|
}
|
|
732
1415
|
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
|
-
|
|
1416
|
+
if (t.kind === "block") {
|
|
1417
|
+
const b = entries(t);
|
|
1418
|
+
const famNode = b.get("targetFamily") ?? b.get("family");
|
|
1419
|
+
let targetFamTuple: UdlFamily | undefined;
|
|
1420
|
+
if (famNode) {
|
|
1421
|
+
const famStr =
|
|
1422
|
+
famNode.kind === "name" ? famNode.value : text(famNode);
|
|
1423
|
+
targetFamTuple = resolveFamily(famStr, famNode);
|
|
1424
|
+
f.targetFamily = targetFamTuple;
|
|
1425
|
+
}
|
|
1426
|
+
if (b.has("target") || b.has("instrument")) {
|
|
1427
|
+
const tgtExpr = (b.get("target") ?? b.get("instrument"))!;
|
|
1428
|
+
const resolvedTgt = resolve(tgtExpr);
|
|
1429
|
+
let tgtVal: string | string[];
|
|
1430
|
+
if (resolvedTgt.kind === "list") {
|
|
1431
|
+
const items = resolvedTgt.items.map((it) =>
|
|
1432
|
+
text(resolve(it)).replaceAll(".", "_"),
|
|
1433
|
+
);
|
|
1434
|
+
tgtVal = items.length === 1 ? items[0]! : items;
|
|
1435
|
+
} else {
|
|
1436
|
+
tgtVal = text(resolvedTgt).replaceAll(".", "_");
|
|
1437
|
+
}
|
|
1438
|
+
f.target = tgtVal;
|
|
1439
|
+
if (targetFamTuple) {
|
|
1440
|
+
checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
|
|
1441
|
+
}
|
|
1442
|
+
} else if (targetFamTuple) {
|
|
1443
|
+
const matched = resolveFamilyInstruments(
|
|
1444
|
+
targetFamTuple,
|
|
1445
|
+
id,
|
|
1446
|
+
famNode!,
|
|
1447
|
+
);
|
|
1448
|
+
f.target = matched.length === 1 ? matched[0] : matched;
|
|
1449
|
+
}
|
|
1450
|
+
f.targetKind =
|
|
1451
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1452
|
+
? "object"
|
|
1453
|
+
: "instrument";
|
|
1454
|
+
} else {
|
|
1455
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
1456
|
+
if (!target)
|
|
1457
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
1458
|
+
const [root, ...tail] = target.split(".");
|
|
1459
|
+
const resolved = environment.get(root!);
|
|
1460
|
+
const resolvedTargets =
|
|
1461
|
+
resolved?.kind === "list"
|
|
1462
|
+
? resolved.items
|
|
1463
|
+
: resolved
|
|
1464
|
+
? [resolved]
|
|
1465
|
+
: [];
|
|
1466
|
+
const targets = resolvedTargets.map((value) =>
|
|
1467
|
+
[text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
|
|
1468
|
+
);
|
|
1469
|
+
f.target =
|
|
1470
|
+
target === "self"
|
|
1471
|
+
? id
|
|
1472
|
+
: targets.length === 1
|
|
1473
|
+
? targets[0]
|
|
1474
|
+
: targets.length
|
|
1475
|
+
? targets
|
|
1476
|
+
: target;
|
|
1477
|
+
f.targetKind =
|
|
1478
|
+
typeof f.target === "string" && objects.has(f.target)
|
|
1479
|
+
? "object"
|
|
1480
|
+
: "instrument";
|
|
1481
|
+
}
|
|
755
1482
|
} else if (type === "account") {
|
|
756
1483
|
if (t.kind === "call") {
|
|
757
1484
|
if (t.args.length < 1 || t.args.length > 4)
|
|
@@ -764,7 +1491,13 @@ export function compile(
|
|
|
764
1491
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
765
1492
|
if (t.args[2]) {
|
|
766
1493
|
const mode = text(t.args[2]);
|
|
767
|
-
if (
|
|
1494
|
+
if (mode === "external")
|
|
1495
|
+
fail(
|
|
1496
|
+
t,
|
|
1497
|
+
"external account mode was removed",
|
|
1498
|
+
"use a reservation and instruction-bound evidence",
|
|
1499
|
+
);
|
|
1500
|
+
if (mode === "contra") f.contra = true;
|
|
768
1501
|
else f.key = mode;
|
|
769
1502
|
}
|
|
770
1503
|
if (t.args[3]) f.key = text(t.args[3]);
|
|
@@ -889,6 +1622,7 @@ export function compile(
|
|
|
889
1622
|
lifecycle.transitions = {};
|
|
890
1623
|
const inst: UdlInstrument = {
|
|
891
1624
|
id,
|
|
1625
|
+
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
892
1626
|
title: title(id),
|
|
893
1627
|
summary: body.has("summary")
|
|
894
1628
|
? String(data(body.get("summary")!))
|
|
@@ -921,9 +1655,9 @@ export function compile(
|
|
|
921
1655
|
// appear after the instrument that asks about its fields.
|
|
922
1656
|
const bound = resolve(binding!);
|
|
923
1657
|
const [root, ...children] = text(bound).split(".");
|
|
924
|
-
const
|
|
925
|
-
let target =
|
|
926
|
-
? templates.get(
|
|
1658
|
+
const assignment = assignments.get(root!);
|
|
1659
|
+
let target = assignment
|
|
1660
|
+
? templates.get(assignment.target)?.body
|
|
927
1661
|
: program.decls
|
|
928
1662
|
.filter(
|
|
929
1663
|
(decl): decl is InstrumentDecl =>
|
|
@@ -971,6 +1705,221 @@ export function compile(
|
|
|
971
1705
|
}),
|
|
972
1706
|
});
|
|
973
1707
|
const slots = entries(selected(asBlock(row.value)));
|
|
1708
|
+
let actionSubject: UdlActionSubject | undefined;
|
|
1709
|
+
let subjectExpr = slots.get("subject");
|
|
1710
|
+
const boundaryBindings = new Set<string>();
|
|
1711
|
+
const authoredMoves = slots.get("moves");
|
|
1712
|
+
for (const move of authoredMoves?.kind === "list"
|
|
1713
|
+
? authoredMoves.items
|
|
1714
|
+
: authoredMoves
|
|
1715
|
+
? [authoredMoves]
|
|
1716
|
+
: []) {
|
|
1717
|
+
const parts = entries(asBlock(move));
|
|
1718
|
+
const boundary = parts.get("boundary");
|
|
1719
|
+
if (!boundary) continue;
|
|
1720
|
+
if (
|
|
1721
|
+
(parts.has("operation")
|
|
1722
|
+
? String(data(parts.get("operation")!))
|
|
1723
|
+
: "internal_transfer.create") !== "internal_transfer.reserve" ||
|
|
1724
|
+
parts.has("shares") ||
|
|
1725
|
+
parts.has("fee")
|
|
1726
|
+
)
|
|
1727
|
+
fail(
|
|
1728
|
+
move,
|
|
1729
|
+
"boundary dispatch requires a reservation",
|
|
1730
|
+
"reserve the exact amount before dispatch",
|
|
1731
|
+
);
|
|
1732
|
+
const adapterExpr = entries(asBlock(boundary)).get("adapter");
|
|
1733
|
+
if (!adapterExpr)
|
|
1734
|
+
fail(
|
|
1735
|
+
boundary,
|
|
1736
|
+
"boundary needs an adapter",
|
|
1737
|
+
"name a bound ADL adapter",
|
|
1738
|
+
);
|
|
1739
|
+
const binding = text(resolve(adapterExpr!));
|
|
1740
|
+
const target =
|
|
1741
|
+
options.adapterRegistry &&
|
|
1742
|
+
Object.hasOwn(options.adapterRegistry, binding)
|
|
1743
|
+
? options.adapterRegistry[binding]
|
|
1744
|
+
: undefined;
|
|
1745
|
+
if (
|
|
1746
|
+
!target ||
|
|
1747
|
+
!Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
1748
|
+
)
|
|
1749
|
+
fail(
|
|
1750
|
+
boundary,
|
|
1751
|
+
`unknown boundary adapter ${binding}`,
|
|
1752
|
+
"bind the named ADL adapter before compilation",
|
|
1753
|
+
);
|
|
1754
|
+
boundaryBindings.add(binding);
|
|
1755
|
+
}
|
|
1756
|
+
if (boundaryBindings.size) {
|
|
1757
|
+
const block = asBlock(subjectExpr);
|
|
1758
|
+
const declared = new Set(
|
|
1759
|
+
block.entries
|
|
1760
|
+
.filter((entry) => entry.key === "adapter")
|
|
1761
|
+
.flatMap((entry) =>
|
|
1762
|
+
entry.value.kind === "list"
|
|
1763
|
+
? entry.value.items.map(text)
|
|
1764
|
+
: [text(entry.value)],
|
|
1765
|
+
),
|
|
1766
|
+
);
|
|
1767
|
+
subjectExpr = {
|
|
1768
|
+
...block,
|
|
1769
|
+
entries: [
|
|
1770
|
+
...block.entries,
|
|
1771
|
+
...[...boundaryBindings]
|
|
1772
|
+
.filter((binding) => !declared.has(binding))
|
|
1773
|
+
.map((binding) => ({
|
|
1774
|
+
key: "adapter",
|
|
1775
|
+
value: {
|
|
1776
|
+
kind: "name" as const,
|
|
1777
|
+
value: binding,
|
|
1778
|
+
span: row.span,
|
|
1779
|
+
},
|
|
1780
|
+
span: row.span,
|
|
1781
|
+
})),
|
|
1782
|
+
],
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
if (subjectExpr) {
|
|
1786
|
+
const subjectBlock = asBlock(subjectExpr);
|
|
1787
|
+
const directRequirements: UdlSubjectRequirement[] = [];
|
|
1788
|
+
const adapterList: UdlActionSubject["adapters"] = [];
|
|
1789
|
+
for (const entry of subjectBlock.entries) {
|
|
1790
|
+
if (entry.key === "adapter") {
|
|
1791
|
+
const bindingNames =
|
|
1792
|
+
entry.value.kind === "list"
|
|
1793
|
+
? entry.value.items.map(text)
|
|
1794
|
+
: [text(entry.value)];
|
|
1795
|
+
for (const bindingName of bindingNames) {
|
|
1796
|
+
const target = options.adapterRegistry?.[bindingName];
|
|
1797
|
+
if (target) {
|
|
1798
|
+
const { adapter, operation } = target;
|
|
1799
|
+
const opBinding = adapter.operationMap[operation];
|
|
1800
|
+
if (
|
|
1801
|
+
opBinding &&
|
|
1802
|
+
opBinding.subjectRequirements !== undefined
|
|
1803
|
+
) {
|
|
1804
|
+
const validatedRequirements: UdlObjectField[] = [];
|
|
1805
|
+
for (const req of opBinding.subjectRequirements) {
|
|
1806
|
+
const result = udlObjectFieldSchema.safeParse(req);
|
|
1807
|
+
if (!result.success || result.data.optional) {
|
|
1808
|
+
fail(
|
|
1809
|
+
entry,
|
|
1810
|
+
`adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`,
|
|
1811
|
+
"ensure adapter subject requirements conform to UDL schema",
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
validatedRequirements.push(result.data);
|
|
1815
|
+
}
|
|
1816
|
+
const declaration = {
|
|
1817
|
+
provider: adapter.provider,
|
|
1818
|
+
capability: adapter.capability,
|
|
1819
|
+
operation,
|
|
1820
|
+
requirements: validatedRequirements,
|
|
1821
|
+
};
|
|
1822
|
+
const digest = sha256(
|
|
1823
|
+
new TextEncoder().encode(canonicalJson(declaration)),
|
|
1824
|
+
);
|
|
1825
|
+
const snapshot: UdlAdapterSubjectSnapshot = {
|
|
1826
|
+
...declaration,
|
|
1827
|
+
declarationDigest: Array.from(digest, (byte) =>
|
|
1828
|
+
byte.toString(16).padStart(2, "0"),
|
|
1829
|
+
).join(""),
|
|
1830
|
+
};
|
|
1831
|
+
const adapterRenames: Record<string, string> = {};
|
|
1832
|
+
if (attachmentInfo?.renames) {
|
|
1833
|
+
for (const req of snapshot.requirements) {
|
|
1834
|
+
if (attachmentInfo.renames.has(req.name)) {
|
|
1835
|
+
adapterRenames[req.name] = attachmentInfo.renames.get(
|
|
1836
|
+
req.name,
|
|
1837
|
+
)!;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
adapterList.push({
|
|
1842
|
+
binding: bindingName,
|
|
1843
|
+
snapshot,
|
|
1844
|
+
...(Object.keys(adapterRenames).length > 0
|
|
1845
|
+
? { renames: adapterRenames }
|
|
1846
|
+
: {}),
|
|
1847
|
+
});
|
|
1848
|
+
for (const reqField of snapshot.requirements) {
|
|
1849
|
+
const objectField = attachmentInfo?.renames.get(
|
|
1850
|
+
reqField.name,
|
|
1851
|
+
);
|
|
1852
|
+
const targetName = objectField ?? reqField.name;
|
|
1853
|
+
const existing = directRequirements.find(
|
|
1854
|
+
(r) => (r.objectField ?? r.field.name) === targetName,
|
|
1855
|
+
);
|
|
1856
|
+
if (existing) {
|
|
1857
|
+
if (!sameObjectField(existing.field, reqField)) {
|
|
1858
|
+
failWithCode(
|
|
1859
|
+
entry,
|
|
1860
|
+
"subject_field_conflict",
|
|
1861
|
+
`conflicting requirement ${reqField.name} in action ${name}`,
|
|
1862
|
+
"rename or unify the requirement",
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
} else {
|
|
1866
|
+
directRequirements.push({
|
|
1867
|
+
field: { ...reqField },
|
|
1868
|
+
...(objectField ? { objectField } : {}),
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
} else {
|
|
1873
|
+
adapterList.push({
|
|
1874
|
+
binding: bindingName,
|
|
1875
|
+
snapshot: null,
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
} else {
|
|
1879
|
+
adapterList.push({
|
|
1880
|
+
binding: bindingName,
|
|
1881
|
+
snapshot: null,
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
} else {
|
|
1886
|
+
const fieldDef = lowerObjectField(entry, resolve);
|
|
1887
|
+
if (fieldDef.optional) {
|
|
1888
|
+
fail(
|
|
1889
|
+
entry,
|
|
1890
|
+
`subject requirement ${entry.key} cannot be optional`,
|
|
1891
|
+
"remove ? from requirement",
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
const objectField = attachmentInfo?.renames.get(entry.key);
|
|
1895
|
+
const targetName = objectField ?? entry.key;
|
|
1896
|
+
const existing = directRequirements.find(
|
|
1897
|
+
(r) => (r.objectField ?? r.field.name) === targetName,
|
|
1898
|
+
);
|
|
1899
|
+
if (existing) {
|
|
1900
|
+
if (!sameObjectField(existing.field, fieldDef)) {
|
|
1901
|
+
failWithCode(
|
|
1902
|
+
entry,
|
|
1903
|
+
"subject_field_conflict",
|
|
1904
|
+
`conflicting requirement ${entry.key} in action ${name}`,
|
|
1905
|
+
"rename or unify the requirement",
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
} else {
|
|
1909
|
+
directRequirements.push({
|
|
1910
|
+
field: fieldDef,
|
|
1911
|
+
...(objectField ? { objectField } : {}),
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (directRequirements.length > 0 || adapterList.length > 0) {
|
|
1917
|
+
actionSubject = {
|
|
1918
|
+
requirements: directRequirements,
|
|
1919
|
+
adapters: adapterList,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
974
1923
|
const a: UdlAction = {
|
|
975
1924
|
summary: slots.has("summary")
|
|
976
1925
|
? String(data(slots.get("summary")!))
|
|
@@ -981,7 +1930,20 @@ export function compile(
|
|
|
981
1930
|
input: lowerFields(asBlock(slots.get("input"))),
|
|
982
1931
|
requires: [],
|
|
983
1932
|
moves: [],
|
|
1933
|
+
...(actionSubject ? { subject: actionSubject } : {}),
|
|
984
1934
|
};
|
|
1935
|
+
for (const binding of boundaryBindings)
|
|
1936
|
+
if (
|
|
1937
|
+
!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1938
|
+
?.snapshot
|
|
1939
|
+
)
|
|
1940
|
+
fail(
|
|
1941
|
+
row,
|
|
1942
|
+
`boundary adapter ${binding} has no declared requirements`,
|
|
1943
|
+
"declare the adapter subject requirements, including an explicit empty list",
|
|
1944
|
+
);
|
|
1945
|
+
currentAction = a;
|
|
1946
|
+
currentActionName = name;
|
|
985
1947
|
if (name !== "create") {
|
|
986
1948
|
const from = slots.get("from");
|
|
987
1949
|
const to = slots.get("to");
|
|
@@ -999,7 +1961,8 @@ export function compile(
|
|
|
999
1961
|
};
|
|
1000
1962
|
}
|
|
1001
1963
|
for (const [key, expr] of slots) {
|
|
1002
|
-
if (["from", "to", "input", "summary"].includes(key))
|
|
1964
|
+
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
1965
|
+
continue;
|
|
1003
1966
|
if (key === "moves") {
|
|
1004
1967
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
1005
1968
|
for (const [index, move] of moves.entries()) {
|
|
@@ -1033,6 +1996,25 @@ export function compile(
|
|
|
1033
1996
|
const amount = parts.get("amount"),
|
|
1034
1997
|
from = parts.get("from"),
|
|
1035
1998
|
to = parts.get("to");
|
|
1999
|
+
if (amount) {
|
|
2000
|
+
const resolvedAmount = resolve(amount);
|
|
2001
|
+
if (
|
|
2002
|
+
resolvedAmount.kind === "name" &&
|
|
2003
|
+
resolvedAmount.value.startsWith("subject.")
|
|
2004
|
+
) {
|
|
2005
|
+
const subField = resolvedAmount.value.split(".")[1]!;
|
|
2006
|
+
const req = a.subject?.requirements.find(
|
|
2007
|
+
(r) => r.field.name === subField,
|
|
2008
|
+
);
|
|
2009
|
+
if (req && req.field.type !== "money") {
|
|
2010
|
+
fail(
|
|
2011
|
+
amount,
|
|
2012
|
+
`move amount subject.${subField} must have type money`,
|
|
2013
|
+
"use a money field",
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1036
2018
|
if (parts.has("shares")) {
|
|
1037
2019
|
if (
|
|
1038
2020
|
!amount ||
|
|
@@ -1064,7 +2046,7 @@ export function compile(
|
|
|
1064
2046
|
});
|
|
1065
2047
|
if (
|
|
1066
2048
|
recipient.kind !== "name" ||
|
|
1067
|
-
!
|
|
2049
|
+
!isParty(recipient.value) ||
|
|
1068
2050
|
rate.kind !== "percent"
|
|
1069
2051
|
)
|
|
1070
2052
|
fail(
|
|
@@ -1139,6 +2121,19 @@ export function compile(
|
|
|
1139
2121
|
...transfer,
|
|
1140
2122
|
operation: op,
|
|
1141
2123
|
capture: String(data(parts.get("capture")!)),
|
|
2124
|
+
...(parts.has("boundary")
|
|
2125
|
+
? {
|
|
2126
|
+
boundary: {
|
|
2127
|
+
adapter: text(
|
|
2128
|
+
resolve(
|
|
2129
|
+
entries(
|
|
2130
|
+
asBlock(parts.get("boundary")),
|
|
2131
|
+
).get("adapter")!,
|
|
2132
|
+
),
|
|
2133
|
+
),
|
|
2134
|
+
},
|
|
2135
|
+
}
|
|
2136
|
+
: {}),
|
|
1142
2137
|
}
|
|
1143
2138
|
: { ...transfer, operation: op },
|
|
1144
2139
|
);
|
|
@@ -1298,7 +2293,41 @@ export function compile(
|
|
|
1298
2293
|
a.approval = approval;
|
|
1299
2294
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
1300
2295
|
}
|
|
2296
|
+
if (attachmentInfo) {
|
|
2297
|
+
if (attachmentInfo.exposed.has(name)) {
|
|
2298
|
+
a.publicAction = attachmentInfo.exposed.get(name)!;
|
|
2299
|
+
} else {
|
|
2300
|
+
delete a.publicAction;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
1301
2303
|
if (automatic(a.actor)) delete a.publicAction;
|
|
2304
|
+
const checkSubjectPaths = (obj: unknown, span: Span) => {
|
|
2305
|
+
if (typeof obj === "string") {
|
|
2306
|
+
if (obj.startsWith("subject.")) {
|
|
2307
|
+
const subField = obj.split(".")[1]!;
|
|
2308
|
+
const req = a.subject?.requirements.find(
|
|
2309
|
+
(r) => r.field.name === subField,
|
|
2310
|
+
);
|
|
2311
|
+
if (!req) {
|
|
2312
|
+
failWithCode(
|
|
2313
|
+
{ span },
|
|
2314
|
+
"subject_field_unknown",
|
|
2315
|
+
`subject.${subField} names no declared subject requirement in action ${name}`,
|
|
2316
|
+
`declare ${subField} in subject { ... }`,
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
} else if (Array.isArray(obj)) {
|
|
2321
|
+
for (const item of obj) checkSubjectPaths(item, span);
|
|
2322
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
2323
|
+
for (const val of Object.values(obj)) checkSubjectPaths(val, span);
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
checkSubjectPaths(a.requires, row.span);
|
|
2327
|
+
checkSubjectPaths(a.set, row.span);
|
|
2328
|
+
checkSubjectPaths(a.invoke, row.span);
|
|
2329
|
+
currentAction = undefined;
|
|
2330
|
+
currentActionName = undefined;
|
|
1302
2331
|
for (const requirement of a.requires ?? []) {
|
|
1303
2332
|
if (
|
|
1304
2333
|
requirement.kind !== "approval" ||
|
|
@@ -1308,19 +2337,39 @@ export function compile(
|
|
|
1308
2337
|
const action = requirement.action ?? name;
|
|
1309
2338
|
const key = `${id}_${action}_decision`;
|
|
1310
2339
|
const previous = implicitDecisions.get(key);
|
|
1311
|
-
if (
|
|
2340
|
+
if (
|
|
2341
|
+
previous &&
|
|
2342
|
+
(previous.party !== requirement.party ||
|
|
2343
|
+
previous.protectedRequest !==
|
|
2344
|
+
(requirement.protectedRequest ?? "self"))
|
|
2345
|
+
)
|
|
1312
2346
|
fail(
|
|
1313
2347
|
{ span: origin },
|
|
1314
|
-
`action ${action} has
|
|
2348
|
+
`action ${action} has conflicting approval parties or protected requests`,
|
|
1315
2349
|
"use a separate decision action for each party",
|
|
1316
2350
|
);
|
|
1317
2351
|
implicitDecisions.set(key, {
|
|
1318
2352
|
target: id,
|
|
1319
2353
|
action,
|
|
1320
2354
|
party: requirement.party,
|
|
2355
|
+
protectedRequest: requirement.protectedRequest ?? "self",
|
|
1321
2356
|
origin,
|
|
1322
2357
|
});
|
|
1323
2358
|
}
|
|
2359
|
+
for (const requirement of a.subject?.requirements ?? []) {
|
|
2360
|
+
const entry =
|
|
2361
|
+
asBlock(subjectExpr).entries.find(
|
|
2362
|
+
(entry) => entry.key === requirement.field.name,
|
|
2363
|
+
) ??
|
|
2364
|
+
asBlock(subjectExpr).entries.find(
|
|
2365
|
+
(entry) => entry.key === "adapter",
|
|
2366
|
+
);
|
|
2367
|
+
requirementOrigins.set(requirement, {
|
|
2368
|
+
source: declarationSources.get(decl) ?? "program",
|
|
2369
|
+
span: entry?.span ?? row.span,
|
|
2370
|
+
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
1324
2373
|
inst.actions[name] = a;
|
|
1325
2374
|
inst.actionOrder.push(name);
|
|
1326
2375
|
}
|
|
@@ -1333,6 +2382,13 @@ export function compile(
|
|
|
1333
2382
|
path: `$.instruments[${document.instruments.length}]`,
|
|
1334
2383
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
1335
2384
|
});
|
|
2385
|
+
if (familyDeclaration && familyDeclaration.revision !== undefined) {
|
|
2386
|
+
inst.family = {
|
|
2387
|
+
module: familyDeclaration.module,
|
|
2388
|
+
exportPath: familyDeclaration.exportPath,
|
|
2389
|
+
revision: familyDeclaration.revision,
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
1336
2392
|
document.instruments.push(inst);
|
|
1337
2393
|
for (const [key, definition] of records) {
|
|
1338
2394
|
const child: InstrumentDecl = {
|
|
@@ -1342,6 +2398,19 @@ export function compile(
|
|
|
1342
2398
|
body: asBlock(definition),
|
|
1343
2399
|
span: origin,
|
|
1344
2400
|
};
|
|
2401
|
+
declarationSources.set(
|
|
2402
|
+
child,
|
|
2403
|
+
declarationSources.get(decl) ?? "program",
|
|
2404
|
+
);
|
|
2405
|
+
const childFamily = familyDeclaration
|
|
2406
|
+
? {
|
|
2407
|
+
module: familyDeclaration.module,
|
|
2408
|
+
exportPath: `${familyDeclaration.exportPath}.${key}`,
|
|
2409
|
+
...(familyDeclaration.revision !== undefined
|
|
2410
|
+
? { revision: familyDeclaration.revision }
|
|
2411
|
+
: {}),
|
|
2412
|
+
}
|
|
2413
|
+
: undefined;
|
|
1345
2414
|
addInstrument(
|
|
1346
2415
|
child,
|
|
1347
2416
|
`${id}_${key}`,
|
|
@@ -1353,9 +2422,221 @@ export function compile(
|
|
|
1353
2422
|
]),
|
|
1354
2423
|
approvers,
|
|
1355
2424
|
enums,
|
|
2425
|
+
attachmentInfo
|
|
2426
|
+
? { ...attachmentInfo, exposed: new Map() }
|
|
2427
|
+
: undefined,
|
|
2428
|
+
childFamily,
|
|
1356
2429
|
);
|
|
1357
2430
|
}
|
|
1358
2431
|
};
|
|
2432
|
+
const compileObject = (decl: ObjectDecl) => {
|
|
2433
|
+
const body = entries(decl.body);
|
|
2434
|
+
for (const key of body.keys()) {
|
|
2435
|
+
if (
|
|
2436
|
+
key !== "fields" &&
|
|
2437
|
+
key !== "columns" &&
|
|
2438
|
+
!key.startsWith("attach ")
|
|
2439
|
+
) {
|
|
2440
|
+
fail(
|
|
2441
|
+
decl,
|
|
2442
|
+
`unknown object clause ${key}`,
|
|
2443
|
+
"use fields, columns, or attach",
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// 1. Lower authored fields
|
|
2449
|
+
const authoredFields: UdlObjectField[] = [];
|
|
2450
|
+
const authoredNames: string[] = [];
|
|
2451
|
+
const fieldsBlock = asBlock(body.get("fields"));
|
|
2452
|
+
for (const row of fieldsBlock.entries) {
|
|
2453
|
+
if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
|
|
2454
|
+
fail(
|
|
2455
|
+
row,
|
|
2456
|
+
`${row.key} is a reserved object name`,
|
|
2457
|
+
"rename this field",
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
const fieldDef = lowerObjectField(row);
|
|
2461
|
+
authoredFields.push(fieldDef);
|
|
2462
|
+
authoredNames.push(row.key);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// 2. Process attachments
|
|
2466
|
+
const attachments: UdlObjectAttachment[] = [];
|
|
2467
|
+
for (const entry of decl.body.entries) {
|
|
2468
|
+
if (!entry.key.startsWith("attach ")) continue;
|
|
2469
|
+
const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
|
|
2470
|
+
if (!match) {
|
|
2471
|
+
fail(
|
|
2472
|
+
entry,
|
|
2473
|
+
"invalid attach syntax",
|
|
2474
|
+
"write attach name = template { ... }",
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
const attachmentName = match[1]!;
|
|
2478
|
+
const targetTemplate = match[2]!;
|
|
2479
|
+
const template = templates.get(targetTemplate);
|
|
2480
|
+
if (!template) {
|
|
2481
|
+
fail(
|
|
2482
|
+
entry,
|
|
2483
|
+
`unknown instrument ${targetTemplate}`,
|
|
2484
|
+
`add use ${targetTemplate.split(".")[0]} and choose a declared instrument`,
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
const instId = `${decl.name}_${attachmentName}`;
|
|
2488
|
+
|
|
2489
|
+
const attachmentBlock = asBlock(entry.value);
|
|
2490
|
+
const renames = new Map<string, string>();
|
|
2491
|
+
const renameEntries = new Map<string, Entry>();
|
|
2492
|
+
const exposed = new Map<string, string>();
|
|
2493
|
+
const tunableEntries: Entry[] = [];
|
|
2494
|
+
|
|
2495
|
+
for (const row of attachmentBlock.entries) {
|
|
2496
|
+
if (row.key === "rename") {
|
|
2497
|
+
for (const r of asBlock(row.value).entries) {
|
|
2498
|
+
renames.set(r.key, text(r.value));
|
|
2499
|
+
renameEntries.set(r.key, r);
|
|
2500
|
+
}
|
|
2501
|
+
} else if (row.key === "expose") {
|
|
2502
|
+
if (row.value.kind === "call") {
|
|
2503
|
+
const actionName = row.value.name;
|
|
2504
|
+
const publicName = text(row.value.args[0]!);
|
|
2505
|
+
exposed.set(actionName, publicName);
|
|
2506
|
+
}
|
|
2507
|
+
} else {
|
|
2508
|
+
tunableEntries.push(row);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
const parties: Record<string, AttachmentPartyBinding> = {};
|
|
2513
|
+
attachments.push({ name: attachmentName, instrument: instId, parties });
|
|
2514
|
+
|
|
2515
|
+
const tunableBlock: BlockExpr = {
|
|
2516
|
+
kind: "block",
|
|
2517
|
+
entries: tunableEntries,
|
|
2518
|
+
span: entry.value.span,
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
2522
|
+
addInstrument(
|
|
2523
|
+
template,
|
|
2524
|
+
instId,
|
|
2525
|
+
tunableBlock,
|
|
2526
|
+
entry.span,
|
|
2527
|
+
new Map(),
|
|
2528
|
+
new Set(),
|
|
2529
|
+
new Map(),
|
|
2530
|
+
{
|
|
2531
|
+
subjectKindId: decl.name,
|
|
2532
|
+
attachmentName,
|
|
2533
|
+
renames,
|
|
2534
|
+
exposed,
|
|
2535
|
+
parties,
|
|
2536
|
+
},
|
|
2537
|
+
templateFamily ? { ...templateFamily } : undefined,
|
|
2538
|
+
);
|
|
2539
|
+
|
|
2540
|
+
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
2541
|
+
if (attachedInst?.actions.create) {
|
|
2542
|
+
const owned = new Set(
|
|
2543
|
+
attachedInst.calculate.map((node) => node.target),
|
|
2544
|
+
);
|
|
2545
|
+
for (const action of Object.values(attachedInst.actions)) {
|
|
2546
|
+
for (const node of action.calculate ?? []) owned.add(node.target);
|
|
2547
|
+
for (const move of action.moves)
|
|
2548
|
+
if ("capture" in move && move.capture) owned.add(move.capture);
|
|
2549
|
+
}
|
|
2550
|
+
const create = attachedInst.actions.create;
|
|
2551
|
+
for (const field of attachedInst.fields) {
|
|
2552
|
+
if (
|
|
2553
|
+
field.type === "account" ||
|
|
2554
|
+
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
2555
|
+
(field.type === "list" &&
|
|
2556
|
+
field.item === "ref" &&
|
|
2557
|
+
field.targetKind === "instrument") ||
|
|
2558
|
+
field.optional ||
|
|
2559
|
+
"value" in field ||
|
|
2560
|
+
owned.has(field.name)
|
|
2561
|
+
)
|
|
2562
|
+
continue;
|
|
2563
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
2564
|
+
const existing = create.subject.requirements.find(
|
|
2565
|
+
(item) => item.field.name === field.name,
|
|
2566
|
+
);
|
|
2567
|
+
if (existing) {
|
|
2568
|
+
if (canonicalJson(existing.field) !== canonicalJson(field))
|
|
2569
|
+
failWithCode(
|
|
2570
|
+
entry,
|
|
2571
|
+
"subject_field_conflict",
|
|
2572
|
+
`${instId}.create.subject.${field.name} conflicts with its instrument field`,
|
|
2573
|
+
"use the instrument field's type and constraints",
|
|
2574
|
+
);
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
const objectField = renames.get(field.name);
|
|
2578
|
+
const requirement = {
|
|
2579
|
+
field,
|
|
2580
|
+
...(objectField ? { objectField } : {}),
|
|
2581
|
+
};
|
|
2582
|
+
create.subject.requirements.push(requirement);
|
|
2583
|
+
requirementOrigins.set(requirement, {
|
|
2584
|
+
source: declarationSources.get(template) ?? "program",
|
|
2585
|
+
span: entry.span,
|
|
2586
|
+
message: `${instId}.create.fields.${field.name}`,
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
for (const [oldName] of renames) {
|
|
2591
|
+
const found =
|
|
2592
|
+
attachedInst &&
|
|
2593
|
+
Object.values(attachedInst.actions).some((action) =>
|
|
2594
|
+
action.subject?.requirements.some(
|
|
2595
|
+
(requirement) => requirement.field.name === oldName,
|
|
2596
|
+
),
|
|
2597
|
+
);
|
|
2598
|
+
if (!found) {
|
|
2599
|
+
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
2600
|
+
failWithCode(
|
|
2601
|
+
renameEntry,
|
|
2602
|
+
"subject_field_unknown",
|
|
2603
|
+
`rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`,
|
|
2604
|
+
"rename a declared subject requirement",
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// 4. Validate columns
|
|
2611
|
+
const columnsExpr = body.get("columns");
|
|
2612
|
+
let columns: string[] = [];
|
|
2613
|
+
if (columnsExpr) {
|
|
2614
|
+
if (columnsExpr.kind !== "list") {
|
|
2615
|
+
fail(
|
|
2616
|
+
columnsExpr,
|
|
2617
|
+
"columns needs a list of field names",
|
|
2618
|
+
"write columns: [name, ...]",
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
columns = columnsExpr.items.map(text);
|
|
2622
|
+
if (columns.length > 8) {
|
|
2623
|
+
fail(
|
|
2624
|
+
columnsExpr,
|
|
2625
|
+
"at most 8 columns allowed",
|
|
2626
|
+
"choose up to 8 columns",
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
document.objects.push({
|
|
2632
|
+
id: decl.name,
|
|
2633
|
+
title: decl.title,
|
|
2634
|
+
authoredFields: authoredNames,
|
|
2635
|
+
attachments,
|
|
2636
|
+
fields: authoredFields,
|
|
2637
|
+
columns,
|
|
2638
|
+
});
|
|
2639
|
+
};
|
|
1359
2640
|
for (const decl of program.decls) {
|
|
1360
2641
|
if (decl.kind === "instrument") {
|
|
1361
2642
|
if (decl.parameters.length)
|
|
@@ -1366,15 +2647,172 @@ export function compile(
|
|
|
1366
2647
|
);
|
|
1367
2648
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
1368
2649
|
}
|
|
1369
|
-
if (decl.kind === "
|
|
1370
|
-
const template = templates.get(decl.
|
|
2650
|
+
if (decl.kind === "assignment") {
|
|
2651
|
+
const template = templates.get(decl.target);
|
|
1371
2652
|
if (!template)
|
|
1372
2653
|
fail(
|
|
1373
2654
|
decl,
|
|
1374
|
-
`unknown
|
|
1375
|
-
`add use ${decl.
|
|
2655
|
+
`unknown instrument ${decl.target}`,
|
|
2656
|
+
`add use ${decl.target.split(".")[0]} and choose a declared instrument`,
|
|
2657
|
+
);
|
|
2658
|
+
const templateFamily = resolveFamily(decl.target, decl, false);
|
|
2659
|
+
addInstrument(
|
|
2660
|
+
template,
|
|
2661
|
+
decl.name,
|
|
2662
|
+
decl.body,
|
|
2663
|
+
decl.span,
|
|
2664
|
+
new Map(),
|
|
2665
|
+
new Set(),
|
|
2666
|
+
new Map(),
|
|
2667
|
+
undefined,
|
|
2668
|
+
templateFamily ? { ...templateFamily } : undefined,
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
if (decl.kind === "object") {
|
|
2672
|
+
compileObject(decl);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
// Propagate mandatory invoked action requirements
|
|
2676
|
+
let changedInvocations = true;
|
|
2677
|
+
let invocationIterations = 0;
|
|
2678
|
+
while (changedInvocations && invocationIterations < 32) {
|
|
2679
|
+
changedInvocations = false;
|
|
2680
|
+
invocationIterations++;
|
|
2681
|
+
for (const inst of document.instruments) {
|
|
2682
|
+
for (const action of Object.values(inst.actions)) {
|
|
2683
|
+
for (const call of action.invoke ?? []) {
|
|
2684
|
+
if (call.guard) continue;
|
|
2685
|
+
let targetIds: string[] = [];
|
|
2686
|
+
if ("instrument" in call) {
|
|
2687
|
+
targetIds = [call.instrument];
|
|
2688
|
+
} else if ("selection" in call) {
|
|
2689
|
+
targetIds = Array.isArray(call.selection.instrument)
|
|
2690
|
+
? call.selection.instrument
|
|
2691
|
+
: [call.selection.instrument];
|
|
2692
|
+
} else if ("reference" in call) {
|
|
2693
|
+
const refField = resolveField(
|
|
2694
|
+
document,
|
|
2695
|
+
inst,
|
|
2696
|
+
call.reference,
|
|
2697
|
+
action.input,
|
|
2698
|
+
action,
|
|
2699
|
+
);
|
|
2700
|
+
if (
|
|
2701
|
+
refField?.type === "ref" &&
|
|
2702
|
+
refField.targetKind === "instrument"
|
|
2703
|
+
) {
|
|
2704
|
+
targetIds = Array.isArray(refField.target)
|
|
2705
|
+
? refField.target
|
|
2706
|
+
: [refField.target];
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
for (const targetId of targetIds) {
|
|
2710
|
+
const targetInst = document.instruments.find(
|
|
2711
|
+
(i) => i.id === targetId,
|
|
2712
|
+
);
|
|
2713
|
+
if (!targetInst) continue;
|
|
2714
|
+
if (
|
|
2715
|
+
inst.subject &&
|
|
2716
|
+
targetInst.subject &&
|
|
2717
|
+
inst.subject === targetInst.subject
|
|
2718
|
+
) {
|
|
2719
|
+
const targetAction = targetInst.actions[call.action];
|
|
2720
|
+
if (!targetAction?.subject) continue;
|
|
2721
|
+
if (!action.subject) {
|
|
2722
|
+
action.subject = { requirements: [], adapters: [] };
|
|
2723
|
+
}
|
|
2724
|
+
for (const adapter of targetAction.subject.adapters) {
|
|
2725
|
+
if (
|
|
2726
|
+
!action.subject.adapters.some(
|
|
2727
|
+
(existing) => existing.binding === adapter.binding,
|
|
2728
|
+
)
|
|
2729
|
+
) {
|
|
2730
|
+
action.subject.adapters.push(adapter);
|
|
2731
|
+
changedInvocations = true;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
for (const targetReq of targetAction.subject.requirements) {
|
|
2735
|
+
const targetObjFieldName =
|
|
2736
|
+
targetReq.objectField ?? targetReq.field.name;
|
|
2737
|
+
const existing = action.subject.requirements.find(
|
|
2738
|
+
(cr) =>
|
|
2739
|
+
(cr.objectField ?? cr.field.name) === targetObjFieldName,
|
|
2740
|
+
);
|
|
2741
|
+
if (!existing) {
|
|
2742
|
+
const inherited = {
|
|
2743
|
+
field: { ...targetReq.field, name: targetObjFieldName },
|
|
2744
|
+
};
|
|
2745
|
+
action.subject.requirements.push(inherited);
|
|
2746
|
+
requirementOrigins.set(
|
|
2747
|
+
inherited,
|
|
2748
|
+
requirementOrigins.get(targetReq)!,
|
|
2749
|
+
);
|
|
2750
|
+
changedInvocations = true;
|
|
2751
|
+
} else if (
|
|
2752
|
+
!sameObjectField(existing.field, targetReq.field)
|
|
2753
|
+
) {
|
|
2754
|
+
const first = requirementOrigins.get(existing)!;
|
|
2755
|
+
const second = requirementOrigins.get(targetReq)!;
|
|
2756
|
+
throw new CompileFailure({
|
|
2757
|
+
code: "subject_field_conflict",
|
|
2758
|
+
message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
|
|
2759
|
+
fix: "ensure compatible requirement definitions across invoked actions",
|
|
2760
|
+
span: first.span,
|
|
2761
|
+
source: first.source,
|
|
2762
|
+
related: [first, second],
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const kind of document.objects) {
|
|
2773
|
+
const origins = new Map(
|
|
2774
|
+
kind.fields.map((field) => [
|
|
2775
|
+
field.name,
|
|
2776
|
+
`authored field ${kind.id}.${field.name}`,
|
|
2777
|
+
]),
|
|
2778
|
+
);
|
|
2779
|
+
for (const instrument of document.instruments.filter(
|
|
2780
|
+
(item) => item.subject === kind.id,
|
|
2781
|
+
)) {
|
|
2782
|
+
for (const name of instrument.actionOrder) {
|
|
2783
|
+
for (const requirement of instrument.actions[name]!.subject
|
|
2784
|
+
?.requirements ?? []) {
|
|
2785
|
+
const field = {
|
|
2786
|
+
...requirement.field,
|
|
2787
|
+
name: requirement.objectField ?? requirement.field.name,
|
|
2788
|
+
};
|
|
2789
|
+
const previous = kind.fields.find(
|
|
2790
|
+
(item) => item.name === field.name,
|
|
2791
|
+
);
|
|
2792
|
+
const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
|
|
2793
|
+
if (previous && !sameObjectField(previous, field)) {
|
|
2794
|
+
failWithCode(
|
|
2795
|
+
objects.get(kind.id)!,
|
|
2796
|
+
"subject_field_conflict",
|
|
2797
|
+
`${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`,
|
|
2798
|
+
"rename fields with different meanings",
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
if (!previous) {
|
|
2802
|
+
kind.fields.push(field);
|
|
2803
|
+
origins.set(field.name, origin);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
for (const column of kind.columns) {
|
|
2809
|
+
if (!kind.fields.some((field) => field.name === column))
|
|
2810
|
+
failWithCode(
|
|
2811
|
+
objects.get(kind.id)!,
|
|
2812
|
+
"subject_field_unknown",
|
|
2813
|
+
`column ${column} is unknown on ${kind.id}`,
|
|
2814
|
+
"name an authored field or attached requirement",
|
|
1376
2815
|
);
|
|
1377
|
-
addInstrument(template, decl.name, decl.body, decl.span);
|
|
1378
2816
|
}
|
|
1379
2817
|
}
|
|
1380
2818
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
@@ -1472,12 +2910,33 @@ export function compile(
|
|
|
1472
2910
|
(action) =>
|
|
1473
2911
|
action.approval?.action === decision.action &&
|
|
1474
2912
|
action.approval.party === decision.party &&
|
|
1475
|
-
inst.fields.some(
|
|
1476
|
-
(
|
|
1477
|
-
field.type
|
|
1478
|
-
field.target
|
|
1479
|
-
`self.${field.name}`
|
|
1480
|
-
|
|
2913
|
+
inst.fields.some((field) => {
|
|
2914
|
+
if (
|
|
2915
|
+
field.type !== "ref" ||
|
|
2916
|
+
field.target !== decision.target ||
|
|
2917
|
+
`self.${field.name}` !== action.approval?.target
|
|
2918
|
+
)
|
|
2919
|
+
return false;
|
|
2920
|
+
const [root, name, ...tail] =
|
|
2921
|
+
decision.protectedRequest.split(".");
|
|
2922
|
+
const input = name && action.approval.input[name];
|
|
2923
|
+
const request =
|
|
2924
|
+
root === "self"
|
|
2925
|
+
? [action.approval.target, name, ...tail]
|
|
2926
|
+
.filter(Boolean)
|
|
2927
|
+
.join(".")
|
|
2928
|
+
: root === "input" && name
|
|
2929
|
+
? input && "field" in input
|
|
2930
|
+
? [input.field, ...tail].join(".")
|
|
2931
|
+
: materialApprovals.has(action)
|
|
2932
|
+
? [`self.material_${name}`, ...tail].join(".")
|
|
2933
|
+
: undefined
|
|
2934
|
+
: undefined;
|
|
2935
|
+
return (
|
|
2936
|
+
request !== undefined &&
|
|
2937
|
+
action.approval.protectedRequest === request
|
|
2938
|
+
);
|
|
2939
|
+
}),
|
|
1481
2940
|
),
|
|
1482
2941
|
);
|
|
1483
2942
|
if (existing) continue;
|
|
@@ -1487,6 +2946,16 @@ export function compile(
|
|
|
1487
2946
|
`implicit decision name ${id} is already used`,
|
|
1488
2947
|
"rename the conflicting object",
|
|
1489
2948
|
);
|
|
2949
|
+
const protectedRequest =
|
|
2950
|
+
decision.protectedRequest === "self"
|
|
2951
|
+
? "self.target"
|
|
2952
|
+
: decision.protectedRequest.startsWith("self.")
|
|
2953
|
+
? `self.target.${decision.protectedRequest.slice(5)}`
|
|
2954
|
+
: fail(
|
|
2955
|
+
{ span: decision.origin },
|
|
2956
|
+
"implicit approval needs a stored protected request",
|
|
2957
|
+
"declare an explicit decision for an input-based protected request",
|
|
2958
|
+
);
|
|
1490
2959
|
addInstrument(
|
|
1491
2960
|
template,
|
|
1492
2961
|
id,
|
|
@@ -1497,10 +2966,14 @@ export function compile(
|
|
|
1497
2966
|
for: decision.target,
|
|
1498
2967
|
approved_by: decision.party,
|
|
1499
2968
|
action: decision.action,
|
|
2969
|
+
protected_request: protectedRequest,
|
|
1500
2970
|
}).map(([key, value]) => ({
|
|
1501
2971
|
key,
|
|
1502
2972
|
value: {
|
|
1503
|
-
kind:
|
|
2973
|
+
kind:
|
|
2974
|
+
key === "action" || key === "protected_request"
|
|
2975
|
+
? "text"
|
|
2976
|
+
: "name",
|
|
1504
2977
|
value,
|
|
1505
2978
|
span: decision.origin,
|
|
1506
2979
|
},
|
|
@@ -1508,6 +2981,22 @@ export function compile(
|
|
|
1508
2981
|
})),
|
|
1509
2982
|
},
|
|
1510
2983
|
decision.origin,
|
|
2984
|
+
new Map(),
|
|
2985
|
+
new Set(),
|
|
2986
|
+
new Map(),
|
|
2987
|
+
document.instruments.find(
|
|
2988
|
+
(instrument) => instrument.id === decision.target,
|
|
2989
|
+
)?.subject
|
|
2990
|
+
? {
|
|
2991
|
+
subjectKindId: document.instruments.find(
|
|
2992
|
+
(instrument) => instrument.id === decision.target,
|
|
2993
|
+
)!.subject!,
|
|
2994
|
+
attachmentName: id,
|
|
2995
|
+
parties: {},
|
|
2996
|
+
renames: new Map(),
|
|
2997
|
+
exposed: new Map(),
|
|
2998
|
+
}
|
|
2999
|
+
: undefined,
|
|
1511
3000
|
);
|
|
1512
3001
|
}
|
|
1513
3002
|
}
|
|
@@ -1578,7 +3067,7 @@ export function compile(
|
|
|
1578
3067
|
.find((o) => i.path.startsWith(o.path));
|
|
1579
3068
|
return diagnostic(
|
|
1580
3069
|
{
|
|
1581
|
-
code: "HSX1601",
|
|
3070
|
+
code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
|
|
1582
3071
|
message: `${i.path}: ${i.message}`,
|
|
1583
3072
|
fix: i.fix,
|
|
1584
3073
|
span: origin?.span ?? program.span,
|