@hyperscale0/hsx 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/src/ast.d.ts +17 -2
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.js +1 -1
- package/dist/src/compile.d.ts +6 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +1194 -97
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +1 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +52 -9
- package/dist/src/cost.js.map +1 -1
- package/dist/src/headers.d.ts +2 -2
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +3 -2
- package/dist/src/headers.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lex.d.ts +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lex.js +5 -0
- package/dist/src/lex.js.map +1 -1
- package/dist/src/parse.js +91 -5
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +10 -9
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +2 -2
- package/docs/README.md +51 -27
- package/docs/headers.md +43 -40
- package/examples/cost-table.json +40 -324
- package/examples/library.hsx +12 -57
- package/package.json +7 -5
- package/src/ast.ts +11 -1
- package/src/cli.ts +1 -1
- package/src/compile.ts +1630 -114
- package/src/cost.ts +60 -16
- package/src/headers.ts +3 -2
- package/src/index.ts +12 -1
- package/src/lex.ts +5 -0
- package/src/parse.ts +87 -5
- package/src/std-bundle.ts +10 -9
- package/src/version.ts +2 -2
- package/std/approvals.hsx +3 -3
- package/std/collections.hsx +45 -4
- package/std/escrow.hsx +17 -8
- package/std/financing.hsx +292 -42
- package/std/insurance.hsx +4 -5
- package/std/lending.hsx +7 -8
- package/std/marketplace.hsx +90 -6
- package/std/money.hsx +15 -49
- package/std/reporting.hsx +1286 -0
- package/std/travel.hsx +5 -6
package/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,12 +1158,15 @@ 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",
|
|
596
1165
|
"summary",
|
|
597
1166
|
"invariants",
|
|
598
1167
|
"constraints",
|
|
1168
|
+
"reports",
|
|
1169
|
+
"revisioned",
|
|
599
1170
|
].includes(key) &&
|
|
600
1171
|
!key.startsWith("action ")
|
|
601
1172
|
)
|
|
@@ -604,6 +1175,24 @@ export function compile(
|
|
|
604
1175
|
`unknown instrument clause ${key}`,
|
|
605
1176
|
"use fields, lifecycle, actions, invariants, or records",
|
|
606
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
|
+
}
|
|
607
1196
|
const records = entries(asBlock(body.get("records")));
|
|
608
1197
|
if (!inherited.size)
|
|
609
1198
|
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
@@ -616,11 +1205,63 @@ export function compile(
|
|
|
616
1205
|
});
|
|
617
1206
|
const fields: UdlField[] = [];
|
|
618
1207
|
const calculations: UdlCalculation[] = [];
|
|
1208
|
+
let currentAction: UdlAction | undefined;
|
|
1209
|
+
let currentActionName: string | undefined;
|
|
619
1210
|
const path = (expr: Expr): string => {
|
|
620
1211
|
const value = resolve(expr);
|
|
621
1212
|
const name = text(value);
|
|
622
|
-
if (
|
|
623
|
-
|
|
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}`;
|
|
624
1265
|
};
|
|
625
1266
|
const val = (expr: Expr): UdlValue => {
|
|
626
1267
|
const v = resolve(expr);
|
|
@@ -637,8 +1278,57 @@ export function compile(
|
|
|
637
1278
|
return id;
|
|
638
1279
|
const value = resolve(expr);
|
|
639
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
|
+
|
|
640
1330
|
const result = Object.fromEntries(
|
|
641
|
-
[...
|
|
1331
|
+
[...rawEntries].map(([key, value]) => [key, data(value)]),
|
|
642
1332
|
);
|
|
643
1333
|
const selection = result.selection as
|
|
644
1334
|
| { instrument?: unknown }
|
|
@@ -690,55 +1380,86 @@ export function compile(
|
|
|
690
1380
|
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
691
1381
|
const constant =
|
|
692
1382
|
row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
const f: Record<string, unknown> = {
|
|
696
|
-
name: row.key,
|
|
697
|
-
type,
|
|
698
|
-
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
699
|
-
};
|
|
700
|
-
if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
|
|
701
|
-
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
702
|
-
if (t.args.length !== 2)
|
|
703
|
-
fail(
|
|
704
|
-
t,
|
|
705
|
-
"bounded fields need a minimum and maximum",
|
|
706
|
-
"write integer(1, 12) or money(0 SAR, 100 SAR)",
|
|
707
|
-
);
|
|
708
|
-
f.minimum = literal(resolve(t.args[0]!));
|
|
709
|
-
f.maximum = literal(resolve(t.args[1]!));
|
|
710
|
-
}
|
|
1383
|
+
const f = lowerFieldShape(row, resolve);
|
|
1384
|
+
const type = f.type;
|
|
711
1385
|
if (type === "list" && t.kind === "call") {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
}
|
|
718
1395
|
}
|
|
719
1396
|
if (type === "ref") {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
?
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
+
}
|
|
742
1463
|
} else if (type === "account") {
|
|
743
1464
|
if (t.kind === "call") {
|
|
744
1465
|
if (t.args.length < 1 || t.args.length > 4)
|
|
@@ -751,7 +1472,13 @@ export function compile(
|
|
|
751
1472
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
752
1473
|
if (t.args[2]) {
|
|
753
1474
|
const mode = text(t.args[2]);
|
|
754
|
-
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;
|
|
755
1482
|
else f.key = mode;
|
|
756
1483
|
}
|
|
757
1484
|
if (t.args[3]) f.key = text(t.args[3]);
|
|
@@ -876,6 +1603,7 @@ export function compile(
|
|
|
876
1603
|
lifecycle.transitions = {};
|
|
877
1604
|
const inst: UdlInstrument = {
|
|
878
1605
|
id,
|
|
1606
|
+
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
879
1607
|
title: title(id),
|
|
880
1608
|
summary: body.has("summary")
|
|
881
1609
|
? String(data(body.get("summary")!))
|
|
@@ -894,20 +1622,53 @@ export function compile(
|
|
|
894
1622
|
...block,
|
|
895
1623
|
entries: block.entries.flatMap((entry) => {
|
|
896
1624
|
if (!entry.key.startsWith("when ")) return [entry];
|
|
897
|
-
const [, tunable, , choice] = entry.key.split(" ");
|
|
1625
|
+
const [, tunable, relation, choice] = entry.key.split(" ");
|
|
898
1626
|
const binding = environment.get(tunable!);
|
|
899
1627
|
if (!binding)
|
|
900
1628
|
fail(
|
|
901
1629
|
entry,
|
|
902
1630
|
`unknown branch tunable ${tunable}`,
|
|
903
|
-
"name an enum tunable declared by this header",
|
|
1631
|
+
"name an enum or reference tunable declared by this header",
|
|
904
1632
|
);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1633
|
+
let matches: boolean;
|
|
1634
|
+
if (relation === "has") {
|
|
1635
|
+
// Inspect declarations, not lowering order: a bound object may
|
|
1636
|
+
// appear after the instrument that asks about its fields.
|
|
1637
|
+
const bound = resolve(binding!);
|
|
1638
|
+
const [root, ...children] = text(bound).split(".");
|
|
1639
|
+
const assignment = assignments.get(root!);
|
|
1640
|
+
let target = assignment
|
|
1641
|
+
? templates.get(assignment.target)?.body
|
|
1642
|
+
: program.decls
|
|
1643
|
+
.filter(
|
|
1644
|
+
(decl): decl is InstrumentDecl =>
|
|
1645
|
+
decl.kind === "instrument",
|
|
1646
|
+
)
|
|
1647
|
+
.find((decl) => decl.name === root)?.body;
|
|
1648
|
+
for (const child of children) {
|
|
1649
|
+
const record = target
|
|
1650
|
+
? entries(asBlock(entries(target).get("records"))).get(child)
|
|
1651
|
+
: undefined;
|
|
1652
|
+
target = record ? asBlock(record) : undefined;
|
|
1653
|
+
}
|
|
1654
|
+
if (!target)
|
|
1655
|
+
fail(
|
|
1656
|
+
entry,
|
|
1657
|
+
"field branch needs a declared object",
|
|
1658
|
+
"bind a reference to a declared object",
|
|
1659
|
+
);
|
|
1660
|
+
matches = entries(asBlock(entries(target!).get("fields"))).has(
|
|
1661
|
+
choice!,
|
|
910
1662
|
);
|
|
1663
|
+
} else {
|
|
1664
|
+
if (!enums.get(tunable!)?.includes(choice!))
|
|
1665
|
+
fail(
|
|
1666
|
+
entry,
|
|
1667
|
+
`unknown enum branch ${choice}`,
|
|
1668
|
+
"use a declared enum value",
|
|
1669
|
+
);
|
|
1670
|
+
matches = text(binding!) === choice;
|
|
1671
|
+
}
|
|
911
1672
|
const body = asBlock(entry.value);
|
|
912
1673
|
for (const clause of body.entries)
|
|
913
1674
|
if (
|
|
@@ -921,10 +1682,225 @@ export function compile(
|
|
|
921
1682
|
"when permits requirements, calculations, moves and invocations",
|
|
922
1683
|
"keep lifecycle and actor clauses outside the branch",
|
|
923
1684
|
);
|
|
924
|
-
return
|
|
1685
|
+
return matches ? selected(body).entries : [];
|
|
925
1686
|
}),
|
|
926
1687
|
});
|
|
927
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
|
+
}
|
|
928
1904
|
const a: UdlAction = {
|
|
929
1905
|
summary: slots.has("summary")
|
|
930
1906
|
? String(data(slots.get("summary")!))
|
|
@@ -935,7 +1911,20 @@ export function compile(
|
|
|
935
1911
|
input: lowerFields(asBlock(slots.get("input"))),
|
|
936
1912
|
requires: [],
|
|
937
1913
|
moves: [],
|
|
1914
|
+
...(actionSubject ? { subject: actionSubject } : {}),
|
|
938
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;
|
|
939
1928
|
if (name !== "create") {
|
|
940
1929
|
const from = slots.get("from");
|
|
941
1930
|
const to = slots.get("to");
|
|
@@ -953,7 +1942,8 @@ export function compile(
|
|
|
953
1942
|
};
|
|
954
1943
|
}
|
|
955
1944
|
for (const [key, expr] of slots) {
|
|
956
|
-
if (["from", "to", "input", "summary"].includes(key))
|
|
1945
|
+
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
1946
|
+
continue;
|
|
957
1947
|
if (key === "moves") {
|
|
958
1948
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
959
1949
|
for (const [index, move] of moves.entries()) {
|
|
@@ -987,6 +1977,25 @@ export function compile(
|
|
|
987
1977
|
const amount = parts.get("amount"),
|
|
988
1978
|
from = parts.get("from"),
|
|
989
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
|
+
}
|
|
990
1999
|
if (parts.has("shares")) {
|
|
991
2000
|
if (
|
|
992
2001
|
!amount ||
|
|
@@ -1018,7 +2027,7 @@ export function compile(
|
|
|
1018
2027
|
});
|
|
1019
2028
|
if (
|
|
1020
2029
|
recipient.kind !== "name" ||
|
|
1021
|
-
!
|
|
2030
|
+
!isParty(recipient.value) ||
|
|
1022
2031
|
rate.kind !== "percent"
|
|
1023
2032
|
)
|
|
1024
2033
|
fail(
|
|
@@ -1093,6 +2102,19 @@ export function compile(
|
|
|
1093
2102
|
...transfer,
|
|
1094
2103
|
operation: op,
|
|
1095
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
|
+
: {}),
|
|
1096
2118
|
}
|
|
1097
2119
|
: { ...transfer, operation: op },
|
|
1098
2120
|
);
|
|
@@ -1252,7 +2274,41 @@ export function compile(
|
|
|
1252
2274
|
a.approval = approval;
|
|
1253
2275
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
1254
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
|
+
}
|
|
1255
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;
|
|
1256
2312
|
for (const requirement of a.requires ?? []) {
|
|
1257
2313
|
if (
|
|
1258
2314
|
requirement.kind !== "approval" ||
|
|
@@ -1262,23 +2318,43 @@ export function compile(
|
|
|
1262
2318
|
const action = requirement.action ?? name;
|
|
1263
2319
|
const key = `${id}_${action}_decision`;
|
|
1264
2320
|
const previous = implicitDecisions.get(key);
|
|
1265
|
-
if (
|
|
2321
|
+
if (
|
|
2322
|
+
previous &&
|
|
2323
|
+
(previous.party !== requirement.party ||
|
|
2324
|
+
previous.protectedRequest !==
|
|
2325
|
+
(requirement.protectedRequest ?? "self"))
|
|
2326
|
+
)
|
|
1266
2327
|
fail(
|
|
1267
2328
|
{ span: origin },
|
|
1268
|
-
`action ${action} has
|
|
2329
|
+
`action ${action} has conflicting approval parties or protected requests`,
|
|
1269
2330
|
"use a separate decision action for each party",
|
|
1270
2331
|
);
|
|
1271
2332
|
implicitDecisions.set(key, {
|
|
1272
2333
|
target: id,
|
|
1273
2334
|
action,
|
|
1274
2335
|
party: requirement.party,
|
|
2336
|
+
protectedRequest: requirement.protectedRequest ?? "self",
|
|
1275
2337
|
origin,
|
|
1276
2338
|
});
|
|
1277
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
|
+
}
|
|
1278
2354
|
inst.actions[name] = a;
|
|
1279
2355
|
inst.actionOrder.push(name);
|
|
1280
2356
|
}
|
|
1281
|
-
for (const key of ["invariants"] as const)
|
|
2357
|
+
for (const key of ["invariants", "reports", "revisioned"] as const)
|
|
1282
2358
|
if (body.has(key))
|
|
1283
2359
|
(inst as unknown as Record<string, unknown>)[key] = data(
|
|
1284
2360
|
body.get(key)!,
|
|
@@ -1287,6 +2363,13 @@ export function compile(
|
|
|
1287
2363
|
path: `$.instruments[${document.instruments.length}]`,
|
|
1288
2364
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
1289
2365
|
});
|
|
2366
|
+
if (familyDeclaration && familyDeclaration.revision !== undefined) {
|
|
2367
|
+
inst.family = {
|
|
2368
|
+
module: familyDeclaration.module,
|
|
2369
|
+
exportPath: familyDeclaration.exportPath,
|
|
2370
|
+
revision: familyDeclaration.revision,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
1290
2373
|
document.instruments.push(inst);
|
|
1291
2374
|
for (const [key, definition] of records) {
|
|
1292
2375
|
const child: InstrumentDecl = {
|
|
@@ -1296,6 +2379,19 @@ export function compile(
|
|
|
1296
2379
|
body: asBlock(definition),
|
|
1297
2380
|
span: origin,
|
|
1298
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;
|
|
1299
2395
|
addInstrument(
|
|
1300
2396
|
child,
|
|
1301
2397
|
`${id}_${key}`,
|
|
@@ -1307,8 +2403,220 @@ export function compile(
|
|
|
1307
2403
|
]),
|
|
1308
2404
|
approvers,
|
|
1309
2405
|
enums,
|
|
2406
|
+
attachmentInfo
|
|
2407
|
+
? { ...attachmentInfo, exposed: new Map() }
|
|
2408
|
+
: undefined,
|
|
2409
|
+
childFamily,
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
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,
|
|
1310
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
|
+
}
|
|
1311
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
|
+
});
|
|
1312
2620
|
};
|
|
1313
2621
|
for (const decl of program.decls) {
|
|
1314
2622
|
if (decl.kind === "instrument") {
|
|
@@ -1320,15 +2628,172 @@ export function compile(
|
|
|
1320
2628
|
);
|
|
1321
2629
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
1322
2630
|
}
|
|
1323
|
-
if (decl.kind === "
|
|
1324
|
-
const template = templates.get(decl.
|
|
2631
|
+
if (decl.kind === "assignment") {
|
|
2632
|
+
const template = templates.get(decl.target);
|
|
1325
2633
|
if (!template)
|
|
1326
2634
|
fail(
|
|
1327
2635
|
decl,
|
|
1328
|
-
`unknown
|
|
1329
|
-
`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",
|
|
1330
2796
|
);
|
|
1331
|
-
addInstrument(template, decl.name, decl.body, decl.span);
|
|
1332
2797
|
}
|
|
1333
2798
|
}
|
|
1334
2799
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
@@ -1361,7 +2826,7 @@ export function compile(
|
|
|
1361
2826
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
1362
2827
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
1363
2828
|
if (edge.from.some((state) => reachable.has(state)))
|
|
1364
|
-
reachable.add(edge.to);
|
|
2829
|
+
if (edge.to !== "preserve") reachable.add(edge.to);
|
|
1365
2830
|
for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
|
|
1366
2831
|
edge.from = edge.from.filter((state) => reachable.has(state));
|
|
1367
2832
|
if (!edge.from.length) {
|
|
@@ -1426,12 +2891,33 @@ export function compile(
|
|
|
1426
2891
|
(action) =>
|
|
1427
2892
|
action.approval?.action === decision.action &&
|
|
1428
2893
|
action.approval.party === decision.party &&
|
|
1429
|
-
inst.fields.some(
|
|
1430
|
-
(
|
|
1431
|
-
field.type
|
|
1432
|
-
field.target
|
|
1433
|
-
`self.${field.name}`
|
|
1434
|
-
|
|
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
|
+
}),
|
|
1435
2921
|
),
|
|
1436
2922
|
);
|
|
1437
2923
|
if (existing) continue;
|
|
@@ -1441,6 +2927,16 @@ export function compile(
|
|
|
1441
2927
|
`implicit decision name ${id} is already used`,
|
|
1442
2928
|
"rename the conflicting object",
|
|
1443
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
|
+
);
|
|
1444
2940
|
addInstrument(
|
|
1445
2941
|
template,
|
|
1446
2942
|
id,
|
|
@@ -1451,10 +2947,14 @@ export function compile(
|
|
|
1451
2947
|
for: decision.target,
|
|
1452
2948
|
approved_by: decision.party,
|
|
1453
2949
|
action: decision.action,
|
|
2950
|
+
protected_request: protectedRequest,
|
|
1454
2951
|
}).map(([key, value]) => ({
|
|
1455
2952
|
key,
|
|
1456
2953
|
value: {
|
|
1457
|
-
kind:
|
|
2954
|
+
kind:
|
|
2955
|
+
key === "action" || key === "protected_request"
|
|
2956
|
+
? "text"
|
|
2957
|
+
: "name",
|
|
1458
2958
|
value,
|
|
1459
2959
|
span: decision.origin,
|
|
1460
2960
|
},
|
|
@@ -1462,6 +2962,22 @@ export function compile(
|
|
|
1462
2962
|
})),
|
|
1463
2963
|
},
|
|
1464
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,
|
|
1465
2981
|
);
|
|
1466
2982
|
}
|
|
1467
2983
|
}
|
|
@@ -1532,7 +3048,7 @@ export function compile(
|
|
|
1532
3048
|
.find((o) => i.path.startsWith(o.path));
|
|
1533
3049
|
return diagnostic(
|
|
1534
3050
|
{
|
|
1535
|
-
code: "HSX1601",
|
|
3051
|
+
code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
|
|
1536
3052
|
message: `${i.path}: ${i.message}`,
|
|
1537
3053
|
fix: i.fix,
|
|
1538
3054
|
span: origin?.span ?? program.span,
|