@hyperscale0/hsx 5.6.0 → 5.8.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 +33 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +352 -18
- package/dist/src/compile.js.map +1 -1
- package/dist/src/diagnostics.d.ts +2 -0
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +23 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/examples-bundle.js +3 -3
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/std-bundle.js +2 -2
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +45 -11
- package/docs/examples.md +1 -1
- package/docs/headers.md +1 -1
- package/examples/collections.hsx +8 -2
- package/examples/financing.hsx +5 -0
- package/examples/lending.hsx +7 -1
- package/package.json +3 -3
- package/src/compile.ts +458 -30
- package/src/diagnostics.ts +34 -0
- package/src/examples-bundle.ts +3 -3
- package/src/std-bundle.ts +2 -2
- package/src/version.ts +1 -1
- package/std/financing.hsx +6 -2
- package/std/marketplace.hsx +3 -9
package/src/compile.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CompileFailure,
|
|
3
|
+
didYouMean,
|
|
4
|
+
fail,
|
|
5
|
+
failWithCode,
|
|
6
|
+
} from "./diagnostics.ts";
|
|
2
7
|
import { hash as sha256 } from "fast-sha256";
|
|
3
8
|
import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
|
|
4
9
|
import {
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
type UdlField,
|
|
22
27
|
type UdlInstrument,
|
|
23
28
|
type UdlObjectField,
|
|
29
|
+
type UdlSelection,
|
|
24
30
|
type UdlSubjectRequirement,
|
|
25
31
|
type UdlValue,
|
|
26
32
|
} from "@hyperscale0/udl";
|
|
@@ -398,6 +404,18 @@ export function compile(
|
|
|
398
404
|
severity: "error",
|
|
399
405
|
stage,
|
|
400
406
|
});
|
|
407
|
+
// A program instrument lowers once standalone and once per attachment, so
|
|
408
|
+
// one authored mistake can fail each copy at the same place.
|
|
409
|
+
const distinct = (list: Diagnostic[]) =>
|
|
410
|
+
list.filter(
|
|
411
|
+
(d, index) =>
|
|
412
|
+
list.findIndex(
|
|
413
|
+
(other) =>
|
|
414
|
+
other.source === d.source &&
|
|
415
|
+
other.span.start === d.span.start &&
|
|
416
|
+
other.message === d.message,
|
|
417
|
+
) === index,
|
|
418
|
+
);
|
|
401
419
|
if (parsed.diagnostics.length)
|
|
402
420
|
return {
|
|
403
421
|
verdict: "invalid",
|
|
@@ -589,6 +607,10 @@ export function compile(
|
|
|
589
607
|
}
|
|
590
608
|
}
|
|
591
609
|
const origins: CompileOriginMapEntry[] = [];
|
|
610
|
+
// Lowered objects lose their spans, so each keeps the expression that
|
|
611
|
+
// produced it for the name check after lowering.
|
|
612
|
+
const authored = new WeakMap<object, Expr>();
|
|
613
|
+
const declarations = new WeakMap<UdlInstrument, InstrumentDecl>();
|
|
592
614
|
const exposures: { instrument: string; action: string; entry: Entry }[] =
|
|
593
615
|
[];
|
|
594
616
|
const refundBindings = new Map<
|
|
@@ -1179,14 +1201,11 @@ export function compile(
|
|
|
1179
1201
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
1180
1202
|
const type =
|
|
1181
1203
|
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
1182
|
-
|
|
1183
|
-
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
new Set(),
|
|
1188
|
-
!!attachmentInfo && type === "party",
|
|
1189
|
-
);
|
|
1204
|
+
const asWritten =
|
|
1205
|
+
(supplied.has(param.key) && !attachmentInfo) || type === "enum";
|
|
1206
|
+
let v = asWritten
|
|
1207
|
+
? actual
|
|
1208
|
+
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
1190
1209
|
if (
|
|
1191
1210
|
type === "duration" &&
|
|
1192
1211
|
(v.kind === "text" || v.kind === "name") &&
|
|
@@ -1237,7 +1256,16 @@ export function compile(
|
|
|
1237
1256
|
? { role: v.value as SubjectPartyRole }
|
|
1238
1257
|
: { party: v.value };
|
|
1239
1258
|
} else if (type === "ref") {
|
|
1240
|
-
|
|
1259
|
+
// Listed objects resolve like a single binding, so `on: [plan]`
|
|
1260
|
+
// names the sibling attachment that `on: plan` names.
|
|
1261
|
+
const values =
|
|
1262
|
+
v.kind !== "list"
|
|
1263
|
+
? [v]
|
|
1264
|
+
: asWritten
|
|
1265
|
+
? v.items
|
|
1266
|
+
: v.items.map((item) => resolve(item));
|
|
1267
|
+
if (v.kind === "list")
|
|
1268
|
+
environment.set(param.key, { ...v, items: values });
|
|
1241
1269
|
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
1242
1270
|
fail(
|
|
1243
1271
|
v,
|
|
@@ -1488,6 +1516,7 @@ export function compile(
|
|
|
1488
1516
|
"parameterDiagnostics",
|
|
1489
1517
|
"reports",
|
|
1490
1518
|
"revisioned",
|
|
1519
|
+
"scope",
|
|
1491
1520
|
].includes(key) &&
|
|
1492
1521
|
!key.startsWith("action ")
|
|
1493
1522
|
)
|
|
@@ -1586,9 +1615,11 @@ export function compile(
|
|
|
1586
1615
|
};
|
|
1587
1616
|
const val = (expr: Expr): UdlValue => {
|
|
1588
1617
|
const v = resolve(expr);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1618
|
+
if (v.kind !== "name" || ["true", "false"].includes(v.value))
|
|
1619
|
+
return { literal: literal(v) };
|
|
1620
|
+
const value = { field: path(v) };
|
|
1621
|
+
authored.set(value, expr);
|
|
1622
|
+
return value;
|
|
1592
1623
|
};
|
|
1593
1624
|
const data = (expr: Expr): unknown => {
|
|
1594
1625
|
if (
|
|
@@ -1645,6 +1676,7 @@ export function compile(
|
|
|
1645
1676
|
if (k === "family" || k === "instrument") continue;
|
|
1646
1677
|
result[k] = data(v);
|
|
1647
1678
|
}
|
|
1679
|
+
authored.set(result, value);
|
|
1648
1680
|
return result;
|
|
1649
1681
|
}
|
|
1650
1682
|
|
|
@@ -1673,10 +1705,14 @@ export function compile(
|
|
|
1673
1705
|
};
|
|
1674
1706
|
return null;
|
|
1675
1707
|
}
|
|
1708
|
+
authored.set(result, value);
|
|
1676
1709
|
return result;
|
|
1677
1710
|
}
|
|
1678
|
-
if (value.kind === "list")
|
|
1679
|
-
|
|
1711
|
+
if (value.kind === "list") {
|
|
1712
|
+
const items = value.items.map(data).filter((item) => item !== null);
|
|
1713
|
+
authored.set(items, value);
|
|
1714
|
+
return items;
|
|
1715
|
+
}
|
|
1680
1716
|
if (
|
|
1681
1717
|
value.kind === "call" &&
|
|
1682
1718
|
value.name === "child" &&
|
|
@@ -1920,6 +1956,7 @@ export function compile(
|
|
|
1920
1956
|
`unknown calculation ${constant.name}`,
|
|
1921
1957
|
"use rate, sum, minimum, subtract, multiply, divide, before or after",
|
|
1922
1958
|
);
|
|
1959
|
+
authored.set(calculations.at(-1)!, constant);
|
|
1923
1960
|
} else if (type === "enum" && constant.kind === "name")
|
|
1924
1961
|
f.value = constant.value;
|
|
1925
1962
|
else if (type === "money" && constant.kind === "name")
|
|
@@ -1930,14 +1967,26 @@ export function compile(
|
|
|
1930
1967
|
});
|
|
1931
1968
|
else f.value = literal(constant, String(type));
|
|
1932
1969
|
}
|
|
1970
|
+
authored.set(f, row.value);
|
|
1933
1971
|
result.push(f as UdlField);
|
|
1934
1972
|
}
|
|
1935
1973
|
return result;
|
|
1936
1974
|
};
|
|
1937
1975
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
)
|
|
1976
|
+
const lifecycleBlock = asBlock(body.get("lifecycle"));
|
|
1977
|
+
const lifecycle = data(lifecycleBlock) as UdlInstrument["lifecycle"];
|
|
1978
|
+
const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
|
|
1979
|
+
const checkState = (expr: Expr, state: unknown) => {
|
|
1980
|
+
if (typeof state !== "string" || !states.length) return;
|
|
1981
|
+
if (states.includes(state)) return;
|
|
1982
|
+
fail(
|
|
1983
|
+
expr,
|
|
1984
|
+
`\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`,
|
|
1985
|
+
`Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`,
|
|
1986
|
+
);
|
|
1987
|
+
};
|
|
1988
|
+
const initialExpr = entries(lifecycleBlock).get("initial");
|
|
1989
|
+
if (initialExpr) checkState(initialExpr, lifecycle.initial);
|
|
1941
1990
|
lifecycle.transitions = Object.create(
|
|
1942
1991
|
null,
|
|
1943
1992
|
) as UdlInstrument["lifecycle"]["transitions"];
|
|
@@ -2278,7 +2327,7 @@ export function compile(
|
|
|
2278
2327
|
const a: UdlAction = {
|
|
2279
2328
|
summary: slots.has("summary")
|
|
2280
2329
|
? String(data(slots.get("summary")!))
|
|
2281
|
-
:
|
|
2330
|
+
: title(name),
|
|
2282
2331
|
publicAction: camel(`${name}_${id}`),
|
|
2283
2332
|
event: `${id}.${name}`,
|
|
2284
2333
|
actor: "caller",
|
|
@@ -2287,6 +2336,7 @@ export function compile(
|
|
|
2287
2336
|
moves: [],
|
|
2288
2337
|
...(actionSubject ? { subject: actionSubject } : {}),
|
|
2289
2338
|
};
|
|
2339
|
+
authored.set(a, row.value);
|
|
2290
2340
|
for (const binding of boundaryBindings)
|
|
2291
2341
|
if (
|
|
2292
2342
|
!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
@@ -2308,12 +2358,12 @@ export function compile(
|
|
|
2308
2358
|
`action ${name} needs from and to`,
|
|
2309
2359
|
"declare its source and destination states",
|
|
2310
2360
|
);
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
};
|
|
2361
|
+
const sources = from!.kind === "list" ? from!.items : [from!];
|
|
2362
|
+
const fromStates = sources.map((e) => String(data(e)));
|
|
2363
|
+
const toState = String(data(to!));
|
|
2364
|
+
sources.forEach((e, index) => checkState(e, fromStates[index]));
|
|
2365
|
+
if (toState !== "preserve") checkState(to!, toState);
|
|
2366
|
+
lifecycle.transitions[name] = { from: fromStates, to: toState };
|
|
2317
2367
|
}
|
|
2318
2368
|
for (const [key, expr] of slots) {
|
|
2319
2369
|
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
@@ -2321,6 +2371,7 @@ export function compile(
|
|
|
2321
2371
|
if (key === "moves") {
|
|
2322
2372
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
2323
2373
|
for (const [index, move] of moves.entries()) {
|
|
2374
|
+
const lowered = a.moves.length;
|
|
2324
2375
|
const parts = entries(asBlock(move));
|
|
2325
2376
|
const op = parts.has("operation")
|
|
2326
2377
|
? String(data(parts.get("operation")!))
|
|
@@ -2570,6 +2621,8 @@ export function compile(
|
|
|
2570
2621
|
"split percentages must sum to 100%",
|
|
2571
2622
|
"give the recipients exactly 100% together",
|
|
2572
2623
|
);
|
|
2624
|
+
for (const item of a.moves.slice(lowered))
|
|
2625
|
+
authored.set(item, move);
|
|
2573
2626
|
continue;
|
|
2574
2627
|
}
|
|
2575
2628
|
if (!amount || !from || !to)
|
|
@@ -2766,6 +2819,8 @@ export function compile(
|
|
|
2766
2819
|
`unknown transfer instruction ${op}`,
|
|
2767
2820
|
"choose create, reserve, post, or void from internal_transfer",
|
|
2768
2821
|
);
|
|
2822
|
+
for (const item of a.moves.slice(lowered))
|
|
2823
|
+
authored.set(item, move);
|
|
2769
2824
|
}
|
|
2770
2825
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
2771
2826
|
}
|
|
@@ -2839,11 +2894,43 @@ export function compile(
|
|
|
2839
2894
|
inst.actions[name] = a;
|
|
2840
2895
|
inst.actionOrder.push(name);
|
|
2841
2896
|
}
|
|
2842
|
-
for (const key of [
|
|
2897
|
+
for (const key of [
|
|
2898
|
+
"invariants",
|
|
2899
|
+
"reports",
|
|
2900
|
+
"revisioned",
|
|
2901
|
+
"scope",
|
|
2902
|
+
] as const)
|
|
2843
2903
|
if (body.has(key))
|
|
2844
2904
|
(inst as unknown as Record<string, unknown>)[key] = data(
|
|
2845
2905
|
body.get(key)!,
|
|
2846
2906
|
);
|
|
2907
|
+
let usesTax = false;
|
|
2908
|
+
for (const action of Object.values(inst.actions)) {
|
|
2909
|
+
for (const move of action.moves) {
|
|
2910
|
+
if (!("from" in move)) continue;
|
|
2911
|
+
for (const endpoint of ["from", "to"] as const) {
|
|
2912
|
+
if (move[endpoint] === "party.programTax") {
|
|
2913
|
+
move[endpoint] = "self.programTaxPayable";
|
|
2914
|
+
usesTax = true;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (usesTax) {
|
|
2920
|
+
if (inst.fields.some((field) => field.name === "programTaxPayable"))
|
|
2921
|
+
fail(
|
|
2922
|
+
decl,
|
|
2923
|
+
"programTaxPayable is reserved for the tax account",
|
|
2924
|
+
"rename the authored field",
|
|
2925
|
+
);
|
|
2926
|
+
inst.fields.push({
|
|
2927
|
+
name: "programTaxPayable",
|
|
2928
|
+
type: "account",
|
|
2929
|
+
owner: "programOperator",
|
|
2930
|
+
book: "cash",
|
|
2931
|
+
key: "programTax",
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2847
2934
|
const shape = udlInstrumentSchema.safeParse(inst);
|
|
2848
2935
|
if (!shape.success) {
|
|
2849
2936
|
const issue = shape.error.issues[0]!;
|
|
@@ -2878,6 +2965,7 @@ export function compile(
|
|
|
2878
2965
|
};
|
|
2879
2966
|
}
|
|
2880
2967
|
document.instruments.push(inst);
|
|
2968
|
+
declarations.set(inst, decl);
|
|
2881
2969
|
for (const [key, definition] of records) {
|
|
2882
2970
|
const childAttachment = attachmentInfo
|
|
2883
2971
|
? {
|
|
@@ -2943,12 +3031,13 @@ export function compile(
|
|
|
2943
3031
|
if (
|
|
2944
3032
|
key !== "fields" &&
|
|
2945
3033
|
key !== "columns" &&
|
|
3034
|
+
key !== "entryActions" &&
|
|
2946
3035
|
!key.startsWith("attach ")
|
|
2947
3036
|
) {
|
|
2948
3037
|
fail(
|
|
2949
3038
|
decl,
|
|
2950
3039
|
`unknown object clause ${key}`,
|
|
2951
|
-
"use fields, columns, or attach",
|
|
3040
|
+
"use fields, columns, entryActions, or attach",
|
|
2952
3041
|
);
|
|
2953
3042
|
}
|
|
2954
3043
|
}
|
|
@@ -3180,9 +3269,21 @@ export function compile(
|
|
|
3180
3269
|
}
|
|
3181
3270
|
}
|
|
3182
3271
|
|
|
3272
|
+
const entryExpr = body.get("entryActions");
|
|
3273
|
+
let entryActions: string[] | undefined;
|
|
3274
|
+
if (entryExpr) {
|
|
3275
|
+
if (entryExpr.kind !== "list" || !entryExpr.items.length)
|
|
3276
|
+
fail(
|
|
3277
|
+
entryExpr,
|
|
3278
|
+
"entryActions needs a nonempty list",
|
|
3279
|
+
"name exposed create actions bound to actor",
|
|
3280
|
+
);
|
|
3281
|
+
entryActions = entryExpr.items.map(text);
|
|
3282
|
+
}
|
|
3183
3283
|
document.objects.push({
|
|
3184
3284
|
id: decl.name,
|
|
3185
3285
|
title: decl.title,
|
|
3286
|
+
...(entryActions ? { entryActions } : {}),
|
|
3186
3287
|
authoredFields: authoredNames,
|
|
3187
3288
|
attachments,
|
|
3188
3289
|
fields: authoredFields,
|
|
@@ -3221,10 +3322,35 @@ export function compile(
|
|
|
3221
3322
|
if (bindingDiagnostics.length)
|
|
3222
3323
|
return {
|
|
3223
3324
|
verdict: "invalid",
|
|
3224
|
-
diagnostics: bindingDiagnostics
|
|
3325
|
+
diagnostics: distinct(bindingDiagnostics)
|
|
3225
3326
|
.sort((a, b) => a.span.start - b.span.start)
|
|
3226
3327
|
.map((d) => diagnostic(d, "check")),
|
|
3227
3328
|
};
|
|
3329
|
+
// Resolve selectors after every attachment and child has its family.
|
|
3330
|
+
const retainSelectionFamilies = (value: unknown): void => {
|
|
3331
|
+
if (!value || typeof value !== "object") return;
|
|
3332
|
+
const record = value as Record<string, unknown>;
|
|
3333
|
+
const selection = record.selection as
|
|
3334
|
+
| { instrument: string | string[]; family?: UdlFamily }
|
|
3335
|
+
| undefined;
|
|
3336
|
+
if (selection && !selection.family) {
|
|
3337
|
+
const families = [selection.instrument]
|
|
3338
|
+
.flat()
|
|
3339
|
+
.map(
|
|
3340
|
+
(id) => document.instruments.find((item) => item.id === id)?.family,
|
|
3341
|
+
);
|
|
3342
|
+
const family = families[0];
|
|
3343
|
+
if (
|
|
3344
|
+
family &&
|
|
3345
|
+
families.every(
|
|
3346
|
+
(item) => canonicalJson(item) === canonicalJson(family),
|
|
3347
|
+
)
|
|
3348
|
+
)
|
|
3349
|
+
selection.family = family;
|
|
3350
|
+
}
|
|
3351
|
+
for (const child of Object.values(record)) retainSelectionFamilies(child);
|
|
3352
|
+
};
|
|
3353
|
+
retainSelectionFamilies(document);
|
|
3228
3354
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
3229
3355
|
let changedInvocations = true;
|
|
3230
3356
|
let invocationIterations = 0;
|
|
@@ -3446,6 +3572,287 @@ export function compile(
|
|
|
3446
3572
|
);
|
|
3447
3573
|
}
|
|
3448
3574
|
}
|
|
3575
|
+
// Names resolve here, once tunables, attachments and derived fields are
|
|
3576
|
+
// final, and each failure points at the expression that wrote the name.
|
|
3577
|
+
type Located = { span: Span; source?: string };
|
|
3578
|
+
type Scope = { inst: UdlInstrument; label: string; action?: UdlAction };
|
|
3579
|
+
const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
|
|
3580
|
+
const site = (owner: object, key: string | number, fallback: Located) => {
|
|
3581
|
+
const expr = authored.get(owner);
|
|
3582
|
+
if (expr?.kind === "block")
|
|
3583
|
+
return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
|
|
3584
|
+
if (expr?.kind === "list" && Array.isArray(owner))
|
|
3585
|
+
return expr.items.length === owner.length
|
|
3586
|
+
? expr.items[Number(key)]!
|
|
3587
|
+
: expr;
|
|
3588
|
+
return expr ?? fallback;
|
|
3589
|
+
};
|
|
3590
|
+
const keySite = (owner: object, key: string, fallback: Located) => {
|
|
3591
|
+
const expr = authored.get(owner);
|
|
3592
|
+
const entry =
|
|
3593
|
+
expr?.kind === "block"
|
|
3594
|
+
? expr.entries.find((item) => item.key === key)
|
|
3595
|
+
: undefined;
|
|
3596
|
+
if (!entry) return fallback;
|
|
3597
|
+
return {
|
|
3598
|
+
...entry,
|
|
3599
|
+
span: { start: entry.span.start, end: entry.span.start + key.length },
|
|
3600
|
+
};
|
|
3601
|
+
};
|
|
3602
|
+
const builtins = (inst: UdlInstrument) => [
|
|
3603
|
+
"id",
|
|
3604
|
+
"now",
|
|
3605
|
+
"createdAt",
|
|
3606
|
+
"productRevision",
|
|
3607
|
+
"status",
|
|
3608
|
+
...(inst.subject ? ["subject"] : []),
|
|
3609
|
+
];
|
|
3610
|
+
const parties = (inst: UdlInstrument) => [
|
|
3611
|
+
...Object.keys(document.parties),
|
|
3612
|
+
...(inst.subject ? subjectPartyRoles : []),
|
|
3613
|
+
];
|
|
3614
|
+
const members = (field: UdlField): string[] => {
|
|
3615
|
+
if (field.type === "account") return ["balance", "reserved"];
|
|
3616
|
+
if (field.type !== "ref") return [];
|
|
3617
|
+
const [id] = [field.target].flat();
|
|
3618
|
+
const target =
|
|
3619
|
+
field.targetKind === "object"
|
|
3620
|
+
? document.objects.find((object) => object.id === id)
|
|
3621
|
+
: byId.get(id!);
|
|
3622
|
+
if (!target) return [];
|
|
3623
|
+
return [
|
|
3624
|
+
...target.fields.map((item) => item.name),
|
|
3625
|
+
...("lifecycle" in target ? builtins(target) : []),
|
|
3626
|
+
];
|
|
3627
|
+
};
|
|
3628
|
+
const checkParty = (name: string, at: Located, inst: UdlInstrument) => {
|
|
3629
|
+
const names = parties(inst);
|
|
3630
|
+
if (!names.includes(name))
|
|
3631
|
+
fail(
|
|
3632
|
+
at,
|
|
3633
|
+
`\`${name}\` is not a declared party.${didYouMean(name, names)}`,
|
|
3634
|
+
`Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
|
|
3635
|
+
);
|
|
3636
|
+
};
|
|
3637
|
+
const checkInput = (
|
|
3638
|
+
name: string,
|
|
3639
|
+
at: Located,
|
|
3640
|
+
label: string,
|
|
3641
|
+
input: readonly UdlField[],
|
|
3642
|
+
) => {
|
|
3643
|
+
const names = input.map((field) => field.name);
|
|
3644
|
+
if (!names.includes(name))
|
|
3645
|
+
fail(
|
|
3646
|
+
at,
|
|
3647
|
+
`\`${name}\` is not an input of \`${label}\`.${didYouMean(name, names)}`,
|
|
3648
|
+
`Declare \`${name}\` in the input of \`${label}\`${names.length ? `, or use one of ${names.join(", ")}` : ""}.`,
|
|
3649
|
+
);
|
|
3650
|
+
};
|
|
3651
|
+
const checkTarget = (id: string, at: Located, objects = false) => {
|
|
3652
|
+
const ids = [
|
|
3653
|
+
...byId.keys(),
|
|
3654
|
+
...(objects ? document.objects.map((object) => object.id) : []),
|
|
3655
|
+
];
|
|
3656
|
+
const noun = objects ? "instrument or object" : "instrument";
|
|
3657
|
+
if (!ids.includes(id))
|
|
3658
|
+
fail(
|
|
3659
|
+
at,
|
|
3660
|
+
`\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`,
|
|
3661
|
+
`Name a declared ${noun}.`,
|
|
3662
|
+
);
|
|
3663
|
+
};
|
|
3664
|
+
const checkPath = (path: string, at: Located, scope: Scope): void => {
|
|
3665
|
+
const { inst, action } = scope;
|
|
3666
|
+
const parts = path.split(".");
|
|
3667
|
+
const found = (count: number) =>
|
|
3668
|
+
resolveField(
|
|
3669
|
+
document,
|
|
3670
|
+
inst,
|
|
3671
|
+
parts.slice(0, count).join("."),
|
|
3672
|
+
action?.input,
|
|
3673
|
+
action,
|
|
3674
|
+
);
|
|
3675
|
+
// Subject paths keep their own check against the action's requirements.
|
|
3676
|
+
if (found(parts.length) || parts[0] === "subject") return;
|
|
3677
|
+
if (parts[0] === "party" && parts[1]) {
|
|
3678
|
+
checkParty(parts[1], at, inst);
|
|
3679
|
+
if (!found(2)) return;
|
|
3680
|
+
}
|
|
3681
|
+
if (!["self", "input", "party"].includes(parts[0]!))
|
|
3682
|
+
parts.unshift("self");
|
|
3683
|
+
let count = 2;
|
|
3684
|
+
while (count <= parts.length && found(count)) count++;
|
|
3685
|
+
if (count > parts.length) return;
|
|
3686
|
+
const name = parts[count - 1]!;
|
|
3687
|
+
if (count > 2) {
|
|
3688
|
+
const prefix = parts.slice(0, count - 1).join(".");
|
|
3689
|
+
const names = members(found(count - 1)!);
|
|
3690
|
+
fail(
|
|
3691
|
+
at,
|
|
3692
|
+
`\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`,
|
|
3693
|
+
names.length
|
|
3694
|
+
? `Use one of ${names.join(", ")}.`
|
|
3695
|
+
: `\`${prefix}\` has no fields.`,
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
if (parts[0] === "input")
|
|
3699
|
+
return checkInput(name, at, scope.label, action?.input ?? []);
|
|
3700
|
+
const declared = declarations.get(inst)!.name;
|
|
3701
|
+
const fields = inst.fields.map((field) => field.name);
|
|
3702
|
+
fail(
|
|
3703
|
+
at,
|
|
3704
|
+
`\`${name}\` is not a field or party of \`${declared}\`.${didYouMean(name, [...fields, ...builtins(inst), ...parties(inst)])}`,
|
|
3705
|
+
`Use a field declared in \`${declared}\` (${fields.join(", ")}) or a declared party (${parties(inst).join(", ")}).`,
|
|
3706
|
+
);
|
|
3707
|
+
};
|
|
3708
|
+
const scopeOf = (inst: UdlInstrument): Scope => ({
|
|
3709
|
+
inst,
|
|
3710
|
+
label: declarations.get(inst)!.name,
|
|
3711
|
+
});
|
|
3712
|
+
const checkSelection = (
|
|
3713
|
+
selection: UdlSelection,
|
|
3714
|
+
at: Located,
|
|
3715
|
+
scope: Scope,
|
|
3716
|
+
) => {
|
|
3717
|
+
walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
|
|
3718
|
+
walk(selection.where, "where", site(selection, "where", at), scope);
|
|
3719
|
+
for (const id of [selection.instrument].flat()) {
|
|
3720
|
+
checkTarget(id, site(selection, "instrument", at));
|
|
3721
|
+
const target = scopeOf(byId.get(id)!);
|
|
3722
|
+
const member = (key: string, where: Located) =>
|
|
3723
|
+
checkPath(
|
|
3724
|
+
key.startsWith("self.") ? key : `self.${key}`,
|
|
3725
|
+
where,
|
|
3726
|
+
target,
|
|
3727
|
+
);
|
|
3728
|
+
for (const key of Object.keys(selection.where ?? {}))
|
|
3729
|
+
member(key, keySite(selection.where!, key, at));
|
|
3730
|
+
member(selection.reference, site(selection, "reference", at));
|
|
3731
|
+
selection.order?.forEach((key, index) =>
|
|
3732
|
+
member(key, site(selection.order!, index, at)),
|
|
3733
|
+
);
|
|
3734
|
+
if (selection.window)
|
|
3735
|
+
member(selection.window.field, site(selection.window, "field", at));
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
const checkCall = (
|
|
3739
|
+
call: NonNullable<UdlAction["invoke"]>[number],
|
|
3740
|
+
at: Located,
|
|
3741
|
+
scope: Scope,
|
|
3742
|
+
) => {
|
|
3743
|
+
const bind = "range" in call ? call.range?.bind : undefined;
|
|
3744
|
+
for (const [key, value] of Object.entries(call))
|
|
3745
|
+
if (key !== "input") walk(value, key, site(call, key, at), scope);
|
|
3746
|
+
for (const [key, value] of Object.entries(call.input))
|
|
3747
|
+
if (!("field" in value) || value.field !== bind)
|
|
3748
|
+
walk(value, key, site(call.input, key, at), scope);
|
|
3749
|
+
if ("instrument" in call)
|
|
3750
|
+
checkTarget(call.instrument, site(call, "instrument", at));
|
|
3751
|
+
const reference =
|
|
3752
|
+
"reference" in call
|
|
3753
|
+
? resolveField(
|
|
3754
|
+
document,
|
|
3755
|
+
scope.inst,
|
|
3756
|
+
call.reference,
|
|
3757
|
+
scope.action?.input,
|
|
3758
|
+
scope.action,
|
|
3759
|
+
)
|
|
3760
|
+
: undefined;
|
|
3761
|
+
const ids =
|
|
3762
|
+
"instrument" in call
|
|
3763
|
+
? [call.instrument]
|
|
3764
|
+
: "selection" in call
|
|
3765
|
+
? [call.selection.instrument].flat()
|
|
3766
|
+
: reference?.type === "ref" && reference.targetKind === "instrument"
|
|
3767
|
+
? [reference.target].flat()
|
|
3768
|
+
: [];
|
|
3769
|
+
for (const id of ids) {
|
|
3770
|
+
const target = byId.get(id);
|
|
3771
|
+
if (!target) continue;
|
|
3772
|
+
const action = target.actions[call.action];
|
|
3773
|
+
if (!action)
|
|
3774
|
+
fail(
|
|
3775
|
+
site(call, "action", at),
|
|
3776
|
+
`\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`,
|
|
3777
|
+
`Use one of ${target.actionOrder.join(", ")}.`,
|
|
3778
|
+
);
|
|
3779
|
+
for (const key of Object.keys(call.input))
|
|
3780
|
+
checkInput(
|
|
3781
|
+
key,
|
|
3782
|
+
keySite(call.input, key, at),
|
|
3783
|
+
`${id}.${call.action}`,
|
|
3784
|
+
action.input,
|
|
3785
|
+
);
|
|
3786
|
+
}
|
|
3787
|
+
};
|
|
3788
|
+
const pathKeys = [
|
|
3789
|
+
"field",
|
|
3790
|
+
"reference",
|
|
3791
|
+
"anchor",
|
|
3792
|
+
"at",
|
|
3793
|
+
"list",
|
|
3794
|
+
"from",
|
|
3795
|
+
"to",
|
|
3796
|
+
"transfer",
|
|
3797
|
+
"subject",
|
|
3798
|
+
"instruction",
|
|
3799
|
+
"installment",
|
|
3800
|
+
"recipient",
|
|
3801
|
+
"dueAt",
|
|
3802
|
+
"fields",
|
|
3803
|
+
];
|
|
3804
|
+
const walk = (
|
|
3805
|
+
value: unknown,
|
|
3806
|
+
key: string,
|
|
3807
|
+
at: Located,
|
|
3808
|
+
scope: Scope,
|
|
3809
|
+
): void => {
|
|
3810
|
+
if (typeof value === "string") {
|
|
3811
|
+
if (key === "party") checkParty(value, at, scope.inst);
|
|
3812
|
+
if (key === "target") checkPath(`self.${value}`, at, scope);
|
|
3813
|
+
if (pathKeys.includes(key)) checkPath(value, at, scope);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
if (!value || typeof value !== "object") return;
|
|
3817
|
+
if (Array.isArray(value))
|
|
3818
|
+
return value.forEach((item, index) =>
|
|
3819
|
+
walk(item, key, site(value, index, at), scope),
|
|
3820
|
+
);
|
|
3821
|
+
if (key === "selection")
|
|
3822
|
+
return checkSelection(value as UdlSelection, at, scope);
|
|
3823
|
+
if (key === "invoke")
|
|
3824
|
+
return checkCall(
|
|
3825
|
+
value as NonNullable<UdlAction["invoke"]>[number],
|
|
3826
|
+
at,
|
|
3827
|
+
scope,
|
|
3828
|
+
);
|
|
3829
|
+
if (key === "set")
|
|
3830
|
+
for (const name of Object.keys(value))
|
|
3831
|
+
checkPath(`self.${name}`, keySite(value, name, at), scope);
|
|
3832
|
+
for (const [child, item] of Object.entries(value))
|
|
3833
|
+
walk(item, child, site(value, child, at), scope);
|
|
3834
|
+
};
|
|
3835
|
+
for (const inst of document.instruments) {
|
|
3836
|
+
const decl = declarations.get(inst)!;
|
|
3837
|
+
const scope = scopeOf(inst);
|
|
3838
|
+
for (const field of [
|
|
3839
|
+
...inst.fields,
|
|
3840
|
+
...Object.values(inst.actions).flatMap((action) => action.input),
|
|
3841
|
+
])
|
|
3842
|
+
if ("target" in field && field.target !== undefined)
|
|
3843
|
+
for (const id of [field.target].flat())
|
|
3844
|
+
checkTarget(id, site(field, "target", decl), true);
|
|
3845
|
+
walk(inst.calculate, "calculate", decl, scope);
|
|
3846
|
+
walk(inst.invariants, "invariants", decl, scope);
|
|
3847
|
+
for (const [name, action] of Object.entries(inst.actions))
|
|
3848
|
+
for (const [key, value] of Object.entries(action))
|
|
3849
|
+
if (key !== "input" && key !== "subject")
|
|
3850
|
+
walk(value, key, site(action, key, decl), {
|
|
3851
|
+
inst,
|
|
3852
|
+
label: `${decl.name}.${name}`,
|
|
3853
|
+
action,
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3449
3856
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
3450
3857
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
3451
3858
|
for (const inst of document.instruments) {
|
|
@@ -3577,6 +3984,27 @@ export function compile(
|
|
|
3577
3984
|
"give each exposed action a distinct public name",
|
|
3578
3985
|
);
|
|
3579
3986
|
}
|
|
3987
|
+
for (const kind of document.objects) {
|
|
3988
|
+
for (const name of kind.entryActions ?? []) {
|
|
3989
|
+
const eligible = kind.attachments.some((attachment) => {
|
|
3990
|
+
const create = document.instruments.find(
|
|
3991
|
+
(instrument) => instrument.id === attachment.instrument,
|
|
3992
|
+
)?.actions.create;
|
|
3993
|
+
return (
|
|
3994
|
+
create?.publicAction === name &&
|
|
3995
|
+
typeof create.actor === "object" &&
|
|
3996
|
+
"party" in create.actor &&
|
|
3997
|
+
create.actor.party === "actor"
|
|
3998
|
+
);
|
|
3999
|
+
});
|
|
4000
|
+
if (!eligible)
|
|
4001
|
+
fail(
|
|
4002
|
+
objects.get(kind.id)!,
|
|
4003
|
+
`entry action ${name} must create an attachment as actor`,
|
|
4004
|
+
"expose its create action and bind its acting party to actor",
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
3580
4008
|
const usedParties = new Set<string>();
|
|
3581
4009
|
const collectParties = (value: unknown): void => {
|
|
3582
4010
|
if (typeof value === "string") {
|
|
@@ -3662,8 +4090,8 @@ export function compile(
|
|
|
3662
4090
|
if (error instanceof CompileFailure)
|
|
3663
4091
|
return {
|
|
3664
4092
|
verdict: "invalid",
|
|
3665
|
-
diagnostics: [...bindingDiagnostics, error.diagnostic].map(
|
|
3666
|
-
diagnostic(d, "check"),
|
|
4093
|
+
diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map(
|
|
4094
|
+
(d) => diagnostic(d, "check"),
|
|
3667
4095
|
),
|
|
3668
4096
|
};
|
|
3669
4097
|
throw error;
|