@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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
# HSX 5.8.0
|
|
2
|
+
|
|
3
|
+
The checker names misspelled fields, parties and states at the misspelled word,
|
|
4
|
+
with the nearest declared name: "`amont` is not a field or party of `deposit`.
|
|
5
|
+
Did you mean `amount`?" This covers moves, actor parties, and field paths in
|
|
6
|
+
`requires`, `unique ... on`, `set`, `calculate:`, `invoke`, `invariants` and the
|
|
7
|
+
`at:` of `due` and `deadline`. The check runs once on the lowered program, so
|
|
8
|
+
it also names a wrong segment deep in a path ("`balanse` is not a field of
|
|
9
|
+
`self.held`"), `input.x` and `party.x` names, invoke instruments, actions and
|
|
10
|
+
inputs, selection instruments, `where` fields and references, `ref<...>`
|
|
11
|
+
targets and the list of an `at` calculation. Swapped adjacent letters count as
|
|
12
|
+
one edit when picking the suggestion. A state typo in `from`, `to` or `initial` is one error
|
|
13
|
+
instead of an invalid transition plus every unreachable state. A program
|
|
14
|
+
instrument attached before its declaration reports each mistake once.
|
|
15
|
+
|
|
16
|
+
A `ref<T>[]` binding accepts a list of sibling attachments, so `on: [plan]`
|
|
17
|
+
compiles to the same program as `on: plan`.
|
|
18
|
+
|
|
19
|
+
# HSX 5.7.0
|
|
20
|
+
|
|
21
|
+
Follows UDL 4.9.0. Object `entryActions` admits enrolled customers through named
|
|
22
|
+
actor-bound create actions. Product-scoped limits resolve across records with
|
|
23
|
+
matching parties. `programTax` moves use an operator-owned pass-through account.
|
|
24
|
+
|
|
25
|
+
Late charge creation accepts a slice reference for plans with multiple instalments.
|
|
26
|
+
Financing, collections and lending samples expose repayment child actions.
|
|
27
|
+
Aggregate selectors retain their selected instrument family, including child
|
|
28
|
+
records, so shared limits compare the matching contracts across Builds.
|
|
29
|
+
|
|
30
|
+
# HSX 5.6.1
|
|
31
|
+
|
|
32
|
+
Action summaries without authored text use the action title without the instrument id.
|
|
33
|
+
|
|
1
34
|
# HSX 5.6.0
|
|
2
35
|
|
|
3
36
|
The compiler records the standard block origin of every instrument compiled
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"AAOA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAeL,KAAK,WAAW,EAQjB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAIL,KAAK,UAAU,EAKf,KAAK,IAAI,EACV,MAAM,UAAU,CAAC;AAClB,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;CACpC;AACD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E;AACD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;CAClE;AACD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,WAAW,CAAC;QACtB,YAAY,EAAE,eAAe,CAAC;QAC9B,SAAS,EAAE,qBAAqB,EAAE,CAAC;KACpC,CAAC;CACH;AAqTD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAwnHf"}
|
package/dist/src/compile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompileFailure, fail, failWithCode } from "./diagnostics.js";
|
|
1
|
+
import { CompileFailure, didYouMean, fail, failWithCode, } from "./diagnostics.js";
|
|
2
2
|
import { hash as sha256 } from "fast-sha256";
|
|
3
3
|
import { buildUdlCostManifest } from "./cost.js";
|
|
4
4
|
import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
|
|
@@ -253,6 +253,11 @@ export function compile(source, options = {}) {
|
|
|
253
253
|
severity: "error",
|
|
254
254
|
stage,
|
|
255
255
|
});
|
|
256
|
+
// A program instrument lowers once standalone and once per attachment, so
|
|
257
|
+
// one authored mistake can fail each copy at the same place.
|
|
258
|
+
const distinct = (list) => list.filter((d, index) => list.findIndex((other) => other.source === d.source &&
|
|
259
|
+
other.span.start === d.span.start &&
|
|
260
|
+
other.message === d.message) === index);
|
|
256
261
|
if (parsed.diagnostics.length)
|
|
257
262
|
return {
|
|
258
263
|
verdict: "invalid",
|
|
@@ -403,6 +408,10 @@ export function compile(source, options = {}) {
|
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
const origins = [];
|
|
411
|
+
// Lowered objects lose their spans, so each keeps the expression that
|
|
412
|
+
// produced it for the name check after lowering.
|
|
413
|
+
const authored = new WeakMap();
|
|
414
|
+
const declarations = new WeakMap();
|
|
406
415
|
const exposures = [];
|
|
407
416
|
const refundBindings = new Map();
|
|
408
417
|
const resolveFamily = (rawPath, expr, required = true) => {
|
|
@@ -807,7 +816,8 @@ export function compile(source, options = {}) {
|
|
|
807
816
|
continue;
|
|
808
817
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
809
818
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
810
|
-
|
|
819
|
+
const asWritten = (supplied.has(param.key) && !attachmentInfo) || type === "enum";
|
|
820
|
+
let v = asWritten
|
|
811
821
|
? actual
|
|
812
822
|
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
813
823
|
if (type === "duration" &&
|
|
@@ -843,7 +853,15 @@ export function compile(source, options = {}) {
|
|
|
843
853
|
: { party: v.value };
|
|
844
854
|
}
|
|
845
855
|
else if (type === "ref") {
|
|
846
|
-
|
|
856
|
+
// Listed objects resolve like a single binding, so `on: [plan]`
|
|
857
|
+
// names the sibling attachment that `on: plan` names.
|
|
858
|
+
const values = v.kind !== "list"
|
|
859
|
+
? [v]
|
|
860
|
+
: asWritten
|
|
861
|
+
? v.items
|
|
862
|
+
: v.items.map((item) => resolve(item));
|
|
863
|
+
if (v.kind === "list")
|
|
864
|
+
environment.set(param.key, { ...v, items: values });
|
|
847
865
|
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
848
866
|
fail(v, `${param.key} accepts one reference`, "use one object name");
|
|
849
867
|
if (!values.length || values.length > 16)
|
|
@@ -1014,6 +1032,7 @@ export function compile(source, options = {}) {
|
|
|
1014
1032
|
"parameterDiagnostics",
|
|
1015
1033
|
"reports",
|
|
1016
1034
|
"revisioned",
|
|
1035
|
+
"scope",
|
|
1017
1036
|
].includes(key) &&
|
|
1018
1037
|
!key.startsWith("action "))
|
|
1019
1038
|
fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
|
|
@@ -1092,9 +1111,11 @@ export function compile(source, options = {}) {
|
|
|
1092
1111
|
};
|
|
1093
1112
|
const val = (expr) => {
|
|
1094
1113
|
const v = resolve(expr);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1114
|
+
if (v.kind !== "name" || ["true", "false"].includes(v.value))
|
|
1115
|
+
return { literal: literal(v) };
|
|
1116
|
+
const value = { field: path(v) };
|
|
1117
|
+
authored.set(value, expr);
|
|
1118
|
+
return value;
|
|
1098
1119
|
};
|
|
1099
1120
|
const data = (expr) => {
|
|
1100
1121
|
if (expr.kind === "call" &&
|
|
@@ -1138,6 +1159,7 @@ export function compile(source, options = {}) {
|
|
|
1138
1159
|
continue;
|
|
1139
1160
|
result[k] = data(v);
|
|
1140
1161
|
}
|
|
1162
|
+
authored.set(result, value);
|
|
1141
1163
|
return result;
|
|
1142
1164
|
}
|
|
1143
1165
|
const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
|
|
@@ -1159,10 +1181,14 @@ export function compile(source, options = {}) {
|
|
|
1159
1181
|
};
|
|
1160
1182
|
return null;
|
|
1161
1183
|
}
|
|
1184
|
+
authored.set(result, value);
|
|
1162
1185
|
return result;
|
|
1163
1186
|
}
|
|
1164
|
-
if (value.kind === "list")
|
|
1165
|
-
|
|
1187
|
+
if (value.kind === "list") {
|
|
1188
|
+
const items = value.items.map(data).filter((item) => item !== null);
|
|
1189
|
+
authored.set(items, value);
|
|
1190
|
+
return items;
|
|
1191
|
+
}
|
|
1166
1192
|
if (value.kind === "call" &&
|
|
1167
1193
|
value.name === "child" &&
|
|
1168
1194
|
value.args.length === 2) {
|
|
@@ -1369,6 +1395,7 @@ export function compile(source, options = {}) {
|
|
|
1369
1395
|
});
|
|
1370
1396
|
else
|
|
1371
1397
|
fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
|
|
1398
|
+
authored.set(calculations.at(-1), constant);
|
|
1372
1399
|
}
|
|
1373
1400
|
else if (type === "enum" && constant.kind === "name")
|
|
1374
1401
|
f.value = constant.value;
|
|
@@ -1381,12 +1408,25 @@ export function compile(source, options = {}) {
|
|
|
1381
1408
|
else
|
|
1382
1409
|
f.value = literal(constant, String(type));
|
|
1383
1410
|
}
|
|
1411
|
+
authored.set(f, row.value);
|
|
1384
1412
|
result.push(f);
|
|
1385
1413
|
}
|
|
1386
1414
|
return result;
|
|
1387
1415
|
};
|
|
1388
1416
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1389
|
-
const
|
|
1417
|
+
const lifecycleBlock = asBlock(body.get("lifecycle"));
|
|
1418
|
+
const lifecycle = data(lifecycleBlock);
|
|
1419
|
+
const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
|
|
1420
|
+
const checkState = (expr, state) => {
|
|
1421
|
+
if (typeof state !== "string" || !states.length)
|
|
1422
|
+
return;
|
|
1423
|
+
if (states.includes(state))
|
|
1424
|
+
return;
|
|
1425
|
+
fail(expr, `\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`, `Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`);
|
|
1426
|
+
};
|
|
1427
|
+
const initialExpr = entries(lifecycleBlock).get("initial");
|
|
1428
|
+
if (initialExpr)
|
|
1429
|
+
checkState(initialExpr, lifecycle.initial);
|
|
1390
1430
|
lifecycle.transitions = Object.create(null);
|
|
1391
1431
|
const inst = {
|
|
1392
1432
|
id,
|
|
@@ -1646,7 +1686,7 @@ export function compile(source, options = {}) {
|
|
|
1646
1686
|
const a = {
|
|
1647
1687
|
summary: slots.has("summary")
|
|
1648
1688
|
? String(data(slots.get("summary")))
|
|
1649
|
-
:
|
|
1689
|
+
: title(name),
|
|
1650
1690
|
publicAction: camel(`${name}_${id}`),
|
|
1651
1691
|
event: `${id}.${name}`,
|
|
1652
1692
|
actor: "caller",
|
|
@@ -1655,6 +1695,7 @@ export function compile(source, options = {}) {
|
|
|
1655
1695
|
moves: [],
|
|
1656
1696
|
...(actionSubject ? { subject: actionSubject } : {}),
|
|
1657
1697
|
};
|
|
1698
|
+
authored.set(a, row.value);
|
|
1658
1699
|
for (const binding of boundaryBindings)
|
|
1659
1700
|
if (!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1660
1701
|
?.snapshot)
|
|
@@ -1666,10 +1707,13 @@ export function compile(source, options = {}) {
|
|
|
1666
1707
|
const to = slots.get("to");
|
|
1667
1708
|
if (!from || !to)
|
|
1668
1709
|
fail(row, `action ${name} needs from and to`, "declare its source and destination states");
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1710
|
+
const sources = from.kind === "list" ? from.items : [from];
|
|
1711
|
+
const fromStates = sources.map((e) => String(data(e)));
|
|
1712
|
+
const toState = String(data(to));
|
|
1713
|
+
sources.forEach((e, index) => checkState(e, fromStates[index]));
|
|
1714
|
+
if (toState !== "preserve")
|
|
1715
|
+
checkState(to, toState);
|
|
1716
|
+
lifecycle.transitions[name] = { from: fromStates, to: toState };
|
|
1673
1717
|
}
|
|
1674
1718
|
for (const [key, expr] of slots) {
|
|
1675
1719
|
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
@@ -1677,6 +1721,7 @@ export function compile(source, options = {}) {
|
|
|
1677
1721
|
if (key === "moves") {
|
|
1678
1722
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
1679
1723
|
for (const [index, move] of moves.entries()) {
|
|
1724
|
+
const lowered = a.moves.length;
|
|
1680
1725
|
const parts = entries(asBlock(move));
|
|
1681
1726
|
const op = parts.has("operation")
|
|
1682
1727
|
? String(data(parts.get("operation")))
|
|
@@ -1830,6 +1875,8 @@ export function compile(source, options = {}) {
|
|
|
1830
1875
|
}
|
|
1831
1876
|
if (total !== 10000)
|
|
1832
1877
|
fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
|
|
1878
|
+
for (const item of a.moves.slice(lowered))
|
|
1879
|
+
authored.set(item, move);
|
|
1833
1880
|
continue;
|
|
1834
1881
|
}
|
|
1835
1882
|
if (!amount || !from || !to)
|
|
@@ -1971,6 +2018,8 @@ export function compile(source, options = {}) {
|
|
|
1971
2018
|
});
|
|
1972
2019
|
else
|
|
1973
2020
|
fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
|
|
2021
|
+
for (const item of a.moves.slice(lowered))
|
|
2022
|
+
authored.set(item, move);
|
|
1974
2023
|
}
|
|
1975
2024
|
}
|
|
1976
2025
|
else
|
|
@@ -2033,9 +2082,38 @@ export function compile(source, options = {}) {
|
|
|
2033
2082
|
inst.actions[name] = a;
|
|
2034
2083
|
inst.actionOrder.push(name);
|
|
2035
2084
|
}
|
|
2036
|
-
for (const key of [
|
|
2085
|
+
for (const key of [
|
|
2086
|
+
"invariants",
|
|
2087
|
+
"reports",
|
|
2088
|
+
"revisioned",
|
|
2089
|
+
"scope",
|
|
2090
|
+
])
|
|
2037
2091
|
if (body.has(key))
|
|
2038
2092
|
inst[key] = data(body.get(key));
|
|
2093
|
+
let usesTax = false;
|
|
2094
|
+
for (const action of Object.values(inst.actions)) {
|
|
2095
|
+
for (const move of action.moves) {
|
|
2096
|
+
if (!("from" in move))
|
|
2097
|
+
continue;
|
|
2098
|
+
for (const endpoint of ["from", "to"]) {
|
|
2099
|
+
if (move[endpoint] === "party.programTax") {
|
|
2100
|
+
move[endpoint] = "self.programTaxPayable";
|
|
2101
|
+
usesTax = true;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (usesTax) {
|
|
2107
|
+
if (inst.fields.some((field) => field.name === "programTaxPayable"))
|
|
2108
|
+
fail(decl, "programTaxPayable is reserved for the tax account", "rename the authored field");
|
|
2109
|
+
inst.fields.push({
|
|
2110
|
+
name: "programTaxPayable",
|
|
2111
|
+
type: "account",
|
|
2112
|
+
owner: "programOperator",
|
|
2113
|
+
book: "cash",
|
|
2114
|
+
key: "programTax",
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2039
2117
|
const shape = udlInstrumentSchema.safeParse(inst);
|
|
2040
2118
|
if (!shape.success) {
|
|
2041
2119
|
const issue = shape.error.issues[0];
|
|
@@ -2060,6 +2138,7 @@ export function compile(source, options = {}) {
|
|
|
2060
2138
|
};
|
|
2061
2139
|
}
|
|
2062
2140
|
document.instruments.push(inst);
|
|
2141
|
+
declarations.set(inst, decl);
|
|
2063
2142
|
for (const [key, definition] of records) {
|
|
2064
2143
|
const childAttachment = attachmentInfo
|
|
2065
2144
|
? {
|
|
@@ -2111,8 +2190,9 @@ export function compile(source, options = {}) {
|
|
|
2111
2190
|
for (const key of body.keys()) {
|
|
2112
2191
|
if (key !== "fields" &&
|
|
2113
2192
|
key !== "columns" &&
|
|
2193
|
+
key !== "entryActions" &&
|
|
2114
2194
|
!key.startsWith("attach ")) {
|
|
2115
|
-
fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
|
|
2195
|
+
fail(decl, `unknown object clause ${key}`, "use fields, columns, entryActions, or attach");
|
|
2116
2196
|
}
|
|
2117
2197
|
}
|
|
2118
2198
|
const authoredFields = [];
|
|
@@ -2271,9 +2351,17 @@ export function compile(source, options = {}) {
|
|
|
2271
2351
|
fail(columnsExpr, `The object list selects ${columns.length} columns; this release supports 8. The object may retain all its fields.`, `Remove ${columns.length === 9 ? "one name" : `${columns.length - 8} names`} from \`columns\`, not from \`fields\`.`);
|
|
2272
2352
|
}
|
|
2273
2353
|
}
|
|
2354
|
+
const entryExpr = body.get("entryActions");
|
|
2355
|
+
let entryActions;
|
|
2356
|
+
if (entryExpr) {
|
|
2357
|
+
if (entryExpr.kind !== "list" || !entryExpr.items.length)
|
|
2358
|
+
fail(entryExpr, "entryActions needs a nonempty list", "name exposed create actions bound to actor");
|
|
2359
|
+
entryActions = entryExpr.items.map(text);
|
|
2360
|
+
}
|
|
2274
2361
|
document.objects.push({
|
|
2275
2362
|
id: decl.name,
|
|
2276
2363
|
title: decl.title,
|
|
2364
|
+
...(entryActions ? { entryActions } : {}),
|
|
2277
2365
|
authoredFields: authoredNames,
|
|
2278
2366
|
attachments,
|
|
2279
2367
|
fields: authoredFields,
|
|
@@ -2298,10 +2386,29 @@ export function compile(source, options = {}) {
|
|
|
2298
2386
|
if (bindingDiagnostics.length)
|
|
2299
2387
|
return {
|
|
2300
2388
|
verdict: "invalid",
|
|
2301
|
-
diagnostics: bindingDiagnostics
|
|
2389
|
+
diagnostics: distinct(bindingDiagnostics)
|
|
2302
2390
|
.sort((a, b) => a.span.start - b.span.start)
|
|
2303
2391
|
.map((d) => diagnostic(d, "check")),
|
|
2304
2392
|
};
|
|
2393
|
+
// Resolve selectors after every attachment and child has its family.
|
|
2394
|
+
const retainSelectionFamilies = (value) => {
|
|
2395
|
+
if (!value || typeof value !== "object")
|
|
2396
|
+
return;
|
|
2397
|
+
const record = value;
|
|
2398
|
+
const selection = record.selection;
|
|
2399
|
+
if (selection && !selection.family) {
|
|
2400
|
+
const families = [selection.instrument]
|
|
2401
|
+
.flat()
|
|
2402
|
+
.map((id) => document.instruments.find((item) => item.id === id)?.family);
|
|
2403
|
+
const family = families[0];
|
|
2404
|
+
if (family &&
|
|
2405
|
+
families.every((item) => canonicalJson(item) === canonicalJson(family)))
|
|
2406
|
+
selection.family = family;
|
|
2407
|
+
}
|
|
2408
|
+
for (const child of Object.values(record))
|
|
2409
|
+
retainSelectionFamilies(child);
|
|
2410
|
+
};
|
|
2411
|
+
retainSelectionFamilies(document);
|
|
2305
2412
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
2306
2413
|
let changedInvocations = true;
|
|
2307
2414
|
let invocationIterations = 0;
|
|
@@ -2463,6 +2570,220 @@ export function compile(source, options = {}) {
|
|
|
2463
2570
|
failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
|
|
2464
2571
|
}
|
|
2465
2572
|
}
|
|
2573
|
+
const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
|
|
2574
|
+
const site = (owner, key, fallback) => {
|
|
2575
|
+
const expr = authored.get(owner);
|
|
2576
|
+
if (expr?.kind === "block")
|
|
2577
|
+
return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
|
|
2578
|
+
if (expr?.kind === "list" && Array.isArray(owner))
|
|
2579
|
+
return expr.items.length === owner.length
|
|
2580
|
+
? expr.items[Number(key)]
|
|
2581
|
+
: expr;
|
|
2582
|
+
return expr ?? fallback;
|
|
2583
|
+
};
|
|
2584
|
+
const keySite = (owner, key, fallback) => {
|
|
2585
|
+
const expr = authored.get(owner);
|
|
2586
|
+
const entry = expr?.kind === "block"
|
|
2587
|
+
? expr.entries.find((item) => item.key === key)
|
|
2588
|
+
: undefined;
|
|
2589
|
+
if (!entry)
|
|
2590
|
+
return fallback;
|
|
2591
|
+
return {
|
|
2592
|
+
...entry,
|
|
2593
|
+
span: { start: entry.span.start, end: entry.span.start + key.length },
|
|
2594
|
+
};
|
|
2595
|
+
};
|
|
2596
|
+
const builtins = (inst) => [
|
|
2597
|
+
"id",
|
|
2598
|
+
"now",
|
|
2599
|
+
"createdAt",
|
|
2600
|
+
"productRevision",
|
|
2601
|
+
"status",
|
|
2602
|
+
...(inst.subject ? ["subject"] : []),
|
|
2603
|
+
];
|
|
2604
|
+
const parties = (inst) => [
|
|
2605
|
+
...Object.keys(document.parties),
|
|
2606
|
+
...(inst.subject ? subjectPartyRoles : []),
|
|
2607
|
+
];
|
|
2608
|
+
const members = (field) => {
|
|
2609
|
+
if (field.type === "account")
|
|
2610
|
+
return ["balance", "reserved"];
|
|
2611
|
+
if (field.type !== "ref")
|
|
2612
|
+
return [];
|
|
2613
|
+
const [id] = [field.target].flat();
|
|
2614
|
+
const target = field.targetKind === "object"
|
|
2615
|
+
? document.objects.find((object) => object.id === id)
|
|
2616
|
+
: byId.get(id);
|
|
2617
|
+
if (!target)
|
|
2618
|
+
return [];
|
|
2619
|
+
return [
|
|
2620
|
+
...target.fields.map((item) => item.name),
|
|
2621
|
+
...("lifecycle" in target ? builtins(target) : []),
|
|
2622
|
+
];
|
|
2623
|
+
};
|
|
2624
|
+
const checkParty = (name, at, inst) => {
|
|
2625
|
+
const names = parties(inst);
|
|
2626
|
+
if (!names.includes(name))
|
|
2627
|
+
fail(at, `\`${name}\` is not a declared party.${didYouMean(name, names)}`, `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`);
|
|
2628
|
+
};
|
|
2629
|
+
const checkInput = (name, at, label, input) => {
|
|
2630
|
+
const names = input.map((field) => field.name);
|
|
2631
|
+
if (!names.includes(name))
|
|
2632
|
+
fail(at, `\`${name}\` is not an input of \`${label}\`.${didYouMean(name, names)}`, `Declare \`${name}\` in the input of \`${label}\`${names.length ? `, or use one of ${names.join(", ")}` : ""}.`);
|
|
2633
|
+
};
|
|
2634
|
+
const checkTarget = (id, at, objects = false) => {
|
|
2635
|
+
const ids = [
|
|
2636
|
+
...byId.keys(),
|
|
2637
|
+
...(objects ? document.objects.map((object) => object.id) : []),
|
|
2638
|
+
];
|
|
2639
|
+
const noun = objects ? "instrument or object" : "instrument";
|
|
2640
|
+
if (!ids.includes(id))
|
|
2641
|
+
fail(at, `\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`, `Name a declared ${noun}.`);
|
|
2642
|
+
};
|
|
2643
|
+
const checkPath = (path, at, scope) => {
|
|
2644
|
+
const { inst, action } = scope;
|
|
2645
|
+
const parts = path.split(".");
|
|
2646
|
+
const found = (count) => resolveField(document, inst, parts.slice(0, count).join("."), action?.input, action);
|
|
2647
|
+
// Subject paths keep their own check against the action's requirements.
|
|
2648
|
+
if (found(parts.length) || parts[0] === "subject")
|
|
2649
|
+
return;
|
|
2650
|
+
if (parts[0] === "party" && parts[1]) {
|
|
2651
|
+
checkParty(parts[1], at, inst);
|
|
2652
|
+
if (!found(2))
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
if (!["self", "input", "party"].includes(parts[0]))
|
|
2656
|
+
parts.unshift("self");
|
|
2657
|
+
let count = 2;
|
|
2658
|
+
while (count <= parts.length && found(count))
|
|
2659
|
+
count++;
|
|
2660
|
+
if (count > parts.length)
|
|
2661
|
+
return;
|
|
2662
|
+
const name = parts[count - 1];
|
|
2663
|
+
if (count > 2) {
|
|
2664
|
+
const prefix = parts.slice(0, count - 1).join(".");
|
|
2665
|
+
const names = members(found(count - 1));
|
|
2666
|
+
fail(at, `\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`, names.length
|
|
2667
|
+
? `Use one of ${names.join(", ")}.`
|
|
2668
|
+
: `\`${prefix}\` has no fields.`);
|
|
2669
|
+
}
|
|
2670
|
+
if (parts[0] === "input")
|
|
2671
|
+
return checkInput(name, at, scope.label, action?.input ?? []);
|
|
2672
|
+
const declared = declarations.get(inst).name;
|
|
2673
|
+
const fields = inst.fields.map((field) => field.name);
|
|
2674
|
+
fail(at, `\`${name}\` is not a field or party of \`${declared}\`.${didYouMean(name, [...fields, ...builtins(inst), ...parties(inst)])}`, `Use a field declared in \`${declared}\` (${fields.join(", ")}) or a declared party (${parties(inst).join(", ")}).`);
|
|
2675
|
+
};
|
|
2676
|
+
const scopeOf = (inst) => ({
|
|
2677
|
+
inst,
|
|
2678
|
+
label: declarations.get(inst).name,
|
|
2679
|
+
});
|
|
2680
|
+
const checkSelection = (selection, at, scope) => {
|
|
2681
|
+
walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
|
|
2682
|
+
walk(selection.where, "where", site(selection, "where", at), scope);
|
|
2683
|
+
for (const id of [selection.instrument].flat()) {
|
|
2684
|
+
checkTarget(id, site(selection, "instrument", at));
|
|
2685
|
+
const target = scopeOf(byId.get(id));
|
|
2686
|
+
const member = (key, where) => checkPath(key.startsWith("self.") ? key : `self.${key}`, where, target);
|
|
2687
|
+
for (const key of Object.keys(selection.where ?? {}))
|
|
2688
|
+
member(key, keySite(selection.where, key, at));
|
|
2689
|
+
member(selection.reference, site(selection, "reference", at));
|
|
2690
|
+
selection.order?.forEach((key, index) => member(key, site(selection.order, index, at)));
|
|
2691
|
+
if (selection.window)
|
|
2692
|
+
member(selection.window.field, site(selection.window, "field", at));
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
const checkCall = (call, at, scope) => {
|
|
2696
|
+
const bind = "range" in call ? call.range?.bind : undefined;
|
|
2697
|
+
for (const [key, value] of Object.entries(call))
|
|
2698
|
+
if (key !== "input")
|
|
2699
|
+
walk(value, key, site(call, key, at), scope);
|
|
2700
|
+
for (const [key, value] of Object.entries(call.input))
|
|
2701
|
+
if (!("field" in value) || value.field !== bind)
|
|
2702
|
+
walk(value, key, site(call.input, key, at), scope);
|
|
2703
|
+
if ("instrument" in call)
|
|
2704
|
+
checkTarget(call.instrument, site(call, "instrument", at));
|
|
2705
|
+
const reference = "reference" in call
|
|
2706
|
+
? resolveField(document, scope.inst, call.reference, scope.action?.input, scope.action)
|
|
2707
|
+
: undefined;
|
|
2708
|
+
const ids = "instrument" in call
|
|
2709
|
+
? [call.instrument]
|
|
2710
|
+
: "selection" in call
|
|
2711
|
+
? [call.selection.instrument].flat()
|
|
2712
|
+
: reference?.type === "ref" && reference.targetKind === "instrument"
|
|
2713
|
+
? [reference.target].flat()
|
|
2714
|
+
: [];
|
|
2715
|
+
for (const id of ids) {
|
|
2716
|
+
const target = byId.get(id);
|
|
2717
|
+
if (!target)
|
|
2718
|
+
continue;
|
|
2719
|
+
const action = target.actions[call.action];
|
|
2720
|
+
if (!action)
|
|
2721
|
+
fail(site(call, "action", at), `\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`, `Use one of ${target.actionOrder.join(", ")}.`);
|
|
2722
|
+
for (const key of Object.keys(call.input))
|
|
2723
|
+
checkInput(key, keySite(call.input, key, at), `${id}.${call.action}`, action.input);
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
const pathKeys = [
|
|
2727
|
+
"field",
|
|
2728
|
+
"reference",
|
|
2729
|
+
"anchor",
|
|
2730
|
+
"at",
|
|
2731
|
+
"list",
|
|
2732
|
+
"from",
|
|
2733
|
+
"to",
|
|
2734
|
+
"transfer",
|
|
2735
|
+
"subject",
|
|
2736
|
+
"instruction",
|
|
2737
|
+
"installment",
|
|
2738
|
+
"recipient",
|
|
2739
|
+
"dueAt",
|
|
2740
|
+
"fields",
|
|
2741
|
+
];
|
|
2742
|
+
const walk = (value, key, at, scope) => {
|
|
2743
|
+
if (typeof value === "string") {
|
|
2744
|
+
if (key === "party")
|
|
2745
|
+
checkParty(value, at, scope.inst);
|
|
2746
|
+
if (key === "target")
|
|
2747
|
+
checkPath(`self.${value}`, at, scope);
|
|
2748
|
+
if (pathKeys.includes(key))
|
|
2749
|
+
checkPath(value, at, scope);
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
if (!value || typeof value !== "object")
|
|
2753
|
+
return;
|
|
2754
|
+
if (Array.isArray(value))
|
|
2755
|
+
return value.forEach((item, index) => walk(item, key, site(value, index, at), scope));
|
|
2756
|
+
if (key === "selection")
|
|
2757
|
+
return checkSelection(value, at, scope);
|
|
2758
|
+
if (key === "invoke")
|
|
2759
|
+
return checkCall(value, at, scope);
|
|
2760
|
+
if (key === "set")
|
|
2761
|
+
for (const name of Object.keys(value))
|
|
2762
|
+
checkPath(`self.${name}`, keySite(value, name, at), scope);
|
|
2763
|
+
for (const [child, item] of Object.entries(value))
|
|
2764
|
+
walk(item, child, site(value, child, at), scope);
|
|
2765
|
+
};
|
|
2766
|
+
for (const inst of document.instruments) {
|
|
2767
|
+
const decl = declarations.get(inst);
|
|
2768
|
+
const scope = scopeOf(inst);
|
|
2769
|
+
for (const field of [
|
|
2770
|
+
...inst.fields,
|
|
2771
|
+
...Object.values(inst.actions).flatMap((action) => action.input),
|
|
2772
|
+
])
|
|
2773
|
+
if ("target" in field && field.target !== undefined)
|
|
2774
|
+
for (const id of [field.target].flat())
|
|
2775
|
+
checkTarget(id, site(field, "target", decl), true);
|
|
2776
|
+
walk(inst.calculate, "calculate", decl, scope);
|
|
2777
|
+
walk(inst.invariants, "invariants", decl, scope);
|
|
2778
|
+
for (const [name, action] of Object.entries(inst.actions))
|
|
2779
|
+
for (const [key, value] of Object.entries(action))
|
|
2780
|
+
if (key !== "input" && key !== "subject")
|
|
2781
|
+
walk(value, key, site(action, key, decl), {
|
|
2782
|
+
inst,
|
|
2783
|
+
label: `${decl.name}.${name}`,
|
|
2784
|
+
action,
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2466
2787
|
const eliminatedStates = new Map();
|
|
2467
2788
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
2468
2789
|
for (const inst of document.instruments) {
|
|
@@ -2557,6 +2878,19 @@ export function compile(source, options = {}) {
|
|
|
2557
2878
|
if (Object.values(instrument.actions).filter((action) => action.publicAction === decl.name).length > 1)
|
|
2558
2879
|
fail(decl, `public action ${decl.name} is used more than once on ${parts.join(".")}`, "give each exposed action a distinct public name");
|
|
2559
2880
|
}
|
|
2881
|
+
for (const kind of document.objects) {
|
|
2882
|
+
for (const name of kind.entryActions ?? []) {
|
|
2883
|
+
const eligible = kind.attachments.some((attachment) => {
|
|
2884
|
+
const create = document.instruments.find((instrument) => instrument.id === attachment.instrument)?.actions.create;
|
|
2885
|
+
return (create?.publicAction === name &&
|
|
2886
|
+
typeof create.actor === "object" &&
|
|
2887
|
+
"party" in create.actor &&
|
|
2888
|
+
create.actor.party === "actor");
|
|
2889
|
+
});
|
|
2890
|
+
if (!eligible)
|
|
2891
|
+
fail(objects.get(kind.id), `entry action ${name} must create an attachment as actor`, "expose its create action and bind its acting party to actor");
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2560
2894
|
const usedParties = new Set();
|
|
2561
2895
|
const collectParties = (value) => {
|
|
2562
2896
|
if (typeof value === "string") {
|
|
@@ -2631,7 +2965,7 @@ export function compile(source, options = {}) {
|
|
|
2631
2965
|
if (error instanceof CompileFailure)
|
|
2632
2966
|
return {
|
|
2633
2967
|
verdict: "invalid",
|
|
2634
|
-
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
|
|
2968
|
+
diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
|
|
2635
2969
|
};
|
|
2636
2970
|
throw error;
|
|
2637
2971
|
}
|