@hyperscale0/hsx 5.7.0 → 6.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 +32 -0
- package/README.md +3 -4
- package/bin/hsx.ts +0 -5
- package/dist/bin/hsx.d.ts +0 -4
- package/dist/bin/hsx.d.ts.map +1 -1
- package/dist/bin/hsx.js +0 -4
- package/dist/bin/hsx.js.map +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +278 -35
- 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.d.ts.map +1 -1
- package/dist/src/examples-bundle.js +0 -1
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/headers.d.ts +1 -1
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +0 -1
- package/dist/src/headers.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +3 -4
- 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 +16 -19
- package/package.json +5 -6
- package/src/compile.ts +363 -48
- package/src/diagnostics.ts +34 -0
- package/src/examples-bundle.ts +0 -1
- package/src/headers.ts +0 -1
- package/src/std-bundle.ts +3 -4
- package/src/version.ts +1 -1
- package/std/financing.hsx +3 -1
- package/std/marketplace.hsx +3 -9
- package/std/reporting.hsx +105 -676
- package/docs/examples.md +0 -22
- package/docs/headers.md +0 -46
- package/examples/travel.hsx +0 -29
- package/std/travel.hsx +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
# HSX 6.0.0
|
|
2
|
+
|
|
3
|
+
Breaking: the `travel` std header is gone, with its `travel.package` and
|
|
4
|
+
`travel.booking` instruments and the travel agency sample. Programs that
|
|
5
|
+
`use travel` no longer compile; build a booking from `escrow.hold` and an
|
|
6
|
+
authored instrument instead. `reporting.portfolio` drops the
|
|
7
|
+
`origination_cohorts` and `bureau_facility` report definitions, which refused
|
|
8
|
+
every request. The compiler no longer declares `programFines` or `programCosts`;
|
|
9
|
+
`late_charge` routes fines and recovery through its `fines_to` and `costs_to`
|
|
10
|
+
party tunables. `account(owner, cash, external)` is an ordinary keyed account
|
|
11
|
+
instead of a refusal. The package stops generating the header and sample
|
|
12
|
+
index pages and the two llms text files; `hsx headers` prints the instrument
|
|
13
|
+
inventory.
|
|
14
|
+
|
|
15
|
+
# HSX 5.8.0
|
|
16
|
+
|
|
17
|
+
The checker names misspelled fields, parties and states at the misspelled word,
|
|
18
|
+
with the nearest declared name: "`amont` is not a field or party of `deposit`.
|
|
19
|
+
Did you mean `amount`?" This covers moves, actor parties, and field paths in
|
|
20
|
+
`requires`, `unique ... on`, `set`, `calculate:`, `invoke`, `invariants` and the
|
|
21
|
+
`at:` of `due` and `deadline`. The check runs once on the lowered program, so
|
|
22
|
+
it also names a wrong segment deep in a path ("`balanse` is not a field of
|
|
23
|
+
`self.held`"), `input.x` and `party.x` names, invoke instruments, actions and
|
|
24
|
+
inputs, selection instruments, `where` fields and references, `ref<...>`
|
|
25
|
+
targets and the list of an `at` calculation. Swapped adjacent letters count as
|
|
26
|
+
one edit when picking the suggestion. A state typo in `from`, `to` or `initial` is one error
|
|
27
|
+
instead of an invalid transition plus every unreachable state. A program
|
|
28
|
+
instrument attached before its declaration reports each mistake once.
|
|
29
|
+
|
|
30
|
+
A `ref<T>[]` binding accepts a list of sibling attachments, so `on: [plan]`
|
|
31
|
+
compiles to the same program as `on: plan`.
|
|
32
|
+
|
|
1
33
|
# HSX 5.7.0
|
|
2
34
|
|
|
3
35
|
Follows UDL 4.9.0. Object `entryActions` admits enrolled customers through named
|
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ Start with the [first program and rental guide](docs/README.md). Save the
|
|
|
21
21
|
It holds 1,000 SAR, charges a one-time 50 SAR late fee, and returns the remainder.
|
|
22
22
|
The guide explains the fixed business binding and the host needed to execute it.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
The [
|
|
24
|
+
`npx hsx headers` prints every std instrument and its tunables from the bundled
|
|
25
|
+
headers. The [samples](examples/) include both complete action paths and
|
|
26
26
|
compositions that still need child-record actions or adapter bindings. A compile
|
|
27
27
|
pass does not prove that a flow is publicly executable or funded.
|
|
28
28
|
|
|
@@ -56,6 +56,5 @@ Run `bun bin/hsx.ts check examples/rental-deposit.hsx` to use the source CLI.
|
|
|
56
56
|
|
|
57
57
|
Edit prose in `docs/README.md`, programs in `examples/`, and header comments in
|
|
58
58
|
`std/`. Run `bun run generate` after changing these inputs. It emits the header
|
|
59
|
-
and example bundles
|
|
60
|
-
`docs/headers.md`, `docs/examples.md`, `llms.txt` or `llms-full.txt` by hand.
|
|
59
|
+
and example bundles.
|
|
61
60
|
`bun run build` also builds the browser playground.
|
package/bin/hsx.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The `hsx` executable. Everything it does lives in `runCli`; this file only
|
|
4
|
-
* binds that function to the real filesystem and the real streams.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
8
3
|
import { runCli } from "../src/cli.ts";
|
|
9
4
|
|
package/dist/bin/hsx.d.ts
CHANGED
package/dist/bin/hsx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hsx.d.ts","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hsx.d.ts","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":""}
|
package/dist/bin/hsx.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The `hsx` executable. Everything it does lives in `runCli`; this file only
|
|
4
|
-
* binds that function to the real filesystem and the real streams.
|
|
5
|
-
*/
|
|
6
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
7
3
|
import { runCli } from "../src/cli.js";
|
|
8
4
|
process.exitCode = await runCli(process.argv.slice(2), {
|
package/dist/bin/hsx.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hsx.js","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"hsx.js","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,CAAC,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACrD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC1C,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC;CACjE,CAAC,CAAC"}
|
|
@@ -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,CAmmHf"}
|
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",
|
|
@@ -356,8 +361,6 @@ export function compile(source, options = {}) {
|
|
|
356
361
|
parties: Object.assign(Object.create(null), {
|
|
357
362
|
programOperator: { kind: "business", role: "program_operator" },
|
|
358
363
|
programTax: { kind: "business", role: "tax_payable" },
|
|
359
|
-
programFines: { kind: "business", role: "fine_payable" },
|
|
360
|
-
programCosts: { kind: "business", role: "cost_recovery" },
|
|
361
364
|
}),
|
|
362
365
|
objects: [],
|
|
363
366
|
instruments: [],
|
|
@@ -403,6 +406,10 @@ export function compile(source, options = {}) {
|
|
|
403
406
|
}
|
|
404
407
|
}
|
|
405
408
|
const origins = [];
|
|
409
|
+
// Lowered objects lose their spans, so each keeps the expression that
|
|
410
|
+
// produced it for the name check after lowering.
|
|
411
|
+
const authored = new WeakMap();
|
|
412
|
+
const declarations = new WeakMap();
|
|
406
413
|
const exposures = [];
|
|
407
414
|
const refundBindings = new Map();
|
|
408
415
|
const resolveFamily = (rawPath, expr, required = true) => {
|
|
@@ -807,7 +814,8 @@ export function compile(source, options = {}) {
|
|
|
807
814
|
continue;
|
|
808
815
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
809
816
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
810
|
-
|
|
817
|
+
const asWritten = (supplied.has(param.key) && !attachmentInfo) || type === "enum";
|
|
818
|
+
let v = asWritten
|
|
811
819
|
? actual
|
|
812
820
|
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
813
821
|
if (type === "duration" &&
|
|
@@ -843,7 +851,15 @@ export function compile(source, options = {}) {
|
|
|
843
851
|
: { party: v.value };
|
|
844
852
|
}
|
|
845
853
|
else if (type === "ref") {
|
|
846
|
-
|
|
854
|
+
// Listed objects resolve like a single binding, so `on: [plan]`
|
|
855
|
+
// names the sibling attachment that `on: plan` names.
|
|
856
|
+
const values = v.kind !== "list"
|
|
857
|
+
? [v]
|
|
858
|
+
: asWritten
|
|
859
|
+
? v.items
|
|
860
|
+
: v.items.map((item) => resolve(item));
|
|
861
|
+
if (v.kind === "list")
|
|
862
|
+
environment.set(param.key, { ...v, items: values });
|
|
847
863
|
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
848
864
|
fail(v, `${param.key} accepts one reference`, "use one object name");
|
|
849
865
|
if (!values.length || values.length > 16)
|
|
@@ -1093,9 +1109,11 @@ export function compile(source, options = {}) {
|
|
|
1093
1109
|
};
|
|
1094
1110
|
const val = (expr) => {
|
|
1095
1111
|
const v = resolve(expr);
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1112
|
+
if (v.kind !== "name" || ["true", "false"].includes(v.value))
|
|
1113
|
+
return { literal: literal(v) };
|
|
1114
|
+
const value = { field: path(v) };
|
|
1115
|
+
authored.set(value, expr);
|
|
1116
|
+
return value;
|
|
1099
1117
|
};
|
|
1100
1118
|
const data = (expr) => {
|
|
1101
1119
|
if (expr.kind === "call" &&
|
|
@@ -1139,6 +1157,7 @@ export function compile(source, options = {}) {
|
|
|
1139
1157
|
continue;
|
|
1140
1158
|
result[k] = data(v);
|
|
1141
1159
|
}
|
|
1160
|
+
authored.set(result, value);
|
|
1142
1161
|
return result;
|
|
1143
1162
|
}
|
|
1144
1163
|
const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
|
|
@@ -1160,10 +1179,14 @@ export function compile(source, options = {}) {
|
|
|
1160
1179
|
};
|
|
1161
1180
|
return null;
|
|
1162
1181
|
}
|
|
1182
|
+
authored.set(result, value);
|
|
1163
1183
|
return result;
|
|
1164
1184
|
}
|
|
1165
|
-
if (value.kind === "list")
|
|
1166
|
-
|
|
1185
|
+
if (value.kind === "list") {
|
|
1186
|
+
const items = value.items.map(data).filter((item) => item !== null);
|
|
1187
|
+
authored.set(items, value);
|
|
1188
|
+
return items;
|
|
1189
|
+
}
|
|
1167
1190
|
if (value.kind === "call" &&
|
|
1168
1191
|
value.name === "child" &&
|
|
1169
1192
|
value.args.length === 2) {
|
|
@@ -1277,8 +1300,6 @@ export function compile(source, options = {}) {
|
|
|
1277
1300
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
1278
1301
|
if (t.args[2]) {
|
|
1279
1302
|
const mode = text(t.args[2]);
|
|
1280
|
-
if (mode === "external")
|
|
1281
|
-
fail(t, "external account mode was removed", "use a reservation and instruction-bound evidence");
|
|
1282
1303
|
if (mode === "contra")
|
|
1283
1304
|
f.contra = true;
|
|
1284
1305
|
else
|
|
@@ -1370,6 +1391,7 @@ export function compile(source, options = {}) {
|
|
|
1370
1391
|
});
|
|
1371
1392
|
else
|
|
1372
1393
|
fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
|
|
1394
|
+
authored.set(calculations.at(-1), constant);
|
|
1373
1395
|
}
|
|
1374
1396
|
else if (type === "enum" && constant.kind === "name")
|
|
1375
1397
|
f.value = constant.value;
|
|
@@ -1382,12 +1404,25 @@ export function compile(source, options = {}) {
|
|
|
1382
1404
|
else
|
|
1383
1405
|
f.value = literal(constant, String(type));
|
|
1384
1406
|
}
|
|
1407
|
+
authored.set(f, row.value);
|
|
1385
1408
|
result.push(f);
|
|
1386
1409
|
}
|
|
1387
1410
|
return result;
|
|
1388
1411
|
};
|
|
1389
1412
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1390
|
-
const
|
|
1413
|
+
const lifecycleBlock = asBlock(body.get("lifecycle"));
|
|
1414
|
+
const lifecycle = data(lifecycleBlock);
|
|
1415
|
+
const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
|
|
1416
|
+
const checkState = (expr, state) => {
|
|
1417
|
+
if (typeof state !== "string" || !states.length)
|
|
1418
|
+
return;
|
|
1419
|
+
if (states.includes(state))
|
|
1420
|
+
return;
|
|
1421
|
+
fail(expr, `\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`, `Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`);
|
|
1422
|
+
};
|
|
1423
|
+
const initialExpr = entries(lifecycleBlock).get("initial");
|
|
1424
|
+
if (initialExpr)
|
|
1425
|
+
checkState(initialExpr, lifecycle.initial);
|
|
1391
1426
|
lifecycle.transitions = Object.create(null);
|
|
1392
1427
|
const inst = {
|
|
1393
1428
|
id,
|
|
@@ -1656,6 +1691,7 @@ export function compile(source, options = {}) {
|
|
|
1656
1691
|
moves: [],
|
|
1657
1692
|
...(actionSubject ? { subject: actionSubject } : {}),
|
|
1658
1693
|
};
|
|
1694
|
+
authored.set(a, row.value);
|
|
1659
1695
|
for (const binding of boundaryBindings)
|
|
1660
1696
|
if (!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
1661
1697
|
?.snapshot)
|
|
@@ -1667,10 +1703,13 @@ export function compile(source, options = {}) {
|
|
|
1667
1703
|
const to = slots.get("to");
|
|
1668
1704
|
if (!from || !to)
|
|
1669
1705
|
fail(row, `action ${name} needs from and to`, "declare its source and destination states");
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1706
|
+
const sources = from.kind === "list" ? from.items : [from];
|
|
1707
|
+
const fromStates = sources.map((e) => String(data(e)));
|
|
1708
|
+
const toState = String(data(to));
|
|
1709
|
+
sources.forEach((e, index) => checkState(e, fromStates[index]));
|
|
1710
|
+
if (toState !== "preserve")
|
|
1711
|
+
checkState(to, toState);
|
|
1712
|
+
lifecycle.transitions[name] = { from: fromStates, to: toState };
|
|
1674
1713
|
}
|
|
1675
1714
|
for (const [key, expr] of slots) {
|
|
1676
1715
|
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
@@ -1678,6 +1717,7 @@ export function compile(source, options = {}) {
|
|
|
1678
1717
|
if (key === "moves") {
|
|
1679
1718
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
1680
1719
|
for (const [index, move] of moves.entries()) {
|
|
1720
|
+
const lowered = a.moves.length;
|
|
1681
1721
|
const parts = entries(asBlock(move));
|
|
1682
1722
|
const op = parts.has("operation")
|
|
1683
1723
|
? String(data(parts.get("operation")))
|
|
@@ -1831,6 +1871,8 @@ export function compile(source, options = {}) {
|
|
|
1831
1871
|
}
|
|
1832
1872
|
if (total !== 10000)
|
|
1833
1873
|
fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
|
|
1874
|
+
for (const item of a.moves.slice(lowered))
|
|
1875
|
+
authored.set(item, move);
|
|
1834
1876
|
continue;
|
|
1835
1877
|
}
|
|
1836
1878
|
if (!amount || !from || !to)
|
|
@@ -1972,6 +2014,8 @@ export function compile(source, options = {}) {
|
|
|
1972
2014
|
});
|
|
1973
2015
|
else
|
|
1974
2016
|
fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
|
|
2017
|
+
for (const item of a.moves.slice(lowered))
|
|
2018
|
+
authored.set(item, move);
|
|
1975
2019
|
}
|
|
1976
2020
|
}
|
|
1977
2021
|
else
|
|
@@ -2090,6 +2134,7 @@ export function compile(source, options = {}) {
|
|
|
2090
2134
|
};
|
|
2091
2135
|
}
|
|
2092
2136
|
document.instruments.push(inst);
|
|
2137
|
+
declarations.set(inst, decl);
|
|
2093
2138
|
for (const [key, definition] of records) {
|
|
2094
2139
|
const childAttachment = attachmentInfo
|
|
2095
2140
|
? {
|
|
@@ -2337,7 +2382,7 @@ export function compile(source, options = {}) {
|
|
|
2337
2382
|
if (bindingDiagnostics.length)
|
|
2338
2383
|
return {
|
|
2339
2384
|
verdict: "invalid",
|
|
2340
|
-
diagnostics: bindingDiagnostics
|
|
2385
|
+
diagnostics: distinct(bindingDiagnostics)
|
|
2341
2386
|
.sort((a, b) => a.span.start - b.span.start)
|
|
2342
2387
|
.map((d) => diagnostic(d, "check")),
|
|
2343
2388
|
};
|
|
@@ -2521,6 +2566,220 @@ export function compile(source, options = {}) {
|
|
|
2521
2566
|
failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
|
|
2522
2567
|
}
|
|
2523
2568
|
}
|
|
2569
|
+
const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
|
|
2570
|
+
const site = (owner, key, fallback) => {
|
|
2571
|
+
const expr = authored.get(owner);
|
|
2572
|
+
if (expr?.kind === "block")
|
|
2573
|
+
return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
|
|
2574
|
+
if (expr?.kind === "list" && Array.isArray(owner))
|
|
2575
|
+
return expr.items.length === owner.length
|
|
2576
|
+
? expr.items[Number(key)]
|
|
2577
|
+
: expr;
|
|
2578
|
+
return expr ?? fallback;
|
|
2579
|
+
};
|
|
2580
|
+
const keySite = (owner, key, fallback) => {
|
|
2581
|
+
const expr = authored.get(owner);
|
|
2582
|
+
const entry = expr?.kind === "block"
|
|
2583
|
+
? expr.entries.find((item) => item.key === key)
|
|
2584
|
+
: undefined;
|
|
2585
|
+
if (!entry)
|
|
2586
|
+
return fallback;
|
|
2587
|
+
return {
|
|
2588
|
+
...entry,
|
|
2589
|
+
span: { start: entry.span.start, end: entry.span.start + key.length },
|
|
2590
|
+
};
|
|
2591
|
+
};
|
|
2592
|
+
const builtins = (inst) => [
|
|
2593
|
+
"id",
|
|
2594
|
+
"now",
|
|
2595
|
+
"createdAt",
|
|
2596
|
+
"productRevision",
|
|
2597
|
+
"status",
|
|
2598
|
+
...(inst.subject ? ["subject"] : []),
|
|
2599
|
+
];
|
|
2600
|
+
const parties = (inst) => [
|
|
2601
|
+
...Object.keys(document.parties),
|
|
2602
|
+
...(inst.subject ? subjectPartyRoles : []),
|
|
2603
|
+
];
|
|
2604
|
+
const members = (field) => {
|
|
2605
|
+
if (field.type === "account")
|
|
2606
|
+
return ["balance", "reserved"];
|
|
2607
|
+
if (field.type !== "ref")
|
|
2608
|
+
return [];
|
|
2609
|
+
const [id] = [field.target].flat();
|
|
2610
|
+
const target = field.targetKind === "object"
|
|
2611
|
+
? document.objects.find((object) => object.id === id)
|
|
2612
|
+
: byId.get(id);
|
|
2613
|
+
if (!target)
|
|
2614
|
+
return [];
|
|
2615
|
+
return [
|
|
2616
|
+
...target.fields.map((item) => item.name),
|
|
2617
|
+
...("lifecycle" in target ? builtins(target) : []),
|
|
2618
|
+
];
|
|
2619
|
+
};
|
|
2620
|
+
const checkParty = (name, at, inst) => {
|
|
2621
|
+
const names = parties(inst);
|
|
2622
|
+
if (!names.includes(name))
|
|
2623
|
+
fail(at, `\`${name}\` is not a declared party.${didYouMean(name, names)}`, `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`);
|
|
2624
|
+
};
|
|
2625
|
+
const checkInput = (name, at, label, input) => {
|
|
2626
|
+
const names = input.map((field) => field.name);
|
|
2627
|
+
if (!names.includes(name))
|
|
2628
|
+
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(", ")}` : ""}.`);
|
|
2629
|
+
};
|
|
2630
|
+
const checkTarget = (id, at, objects = false) => {
|
|
2631
|
+
const ids = [
|
|
2632
|
+
...byId.keys(),
|
|
2633
|
+
...(objects ? document.objects.map((object) => object.id) : []),
|
|
2634
|
+
];
|
|
2635
|
+
const noun = objects ? "instrument or object" : "instrument";
|
|
2636
|
+
if (!ids.includes(id))
|
|
2637
|
+
fail(at, `\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`, `Name a declared ${noun}.`);
|
|
2638
|
+
};
|
|
2639
|
+
const checkPath = (path, at, scope) => {
|
|
2640
|
+
const { inst, action } = scope;
|
|
2641
|
+
const parts = path.split(".");
|
|
2642
|
+
const found = (count) => resolveField(document, inst, parts.slice(0, count).join("."), action?.input, action);
|
|
2643
|
+
// Subject paths keep their own check against the action's requirements.
|
|
2644
|
+
if (found(parts.length) || parts[0] === "subject")
|
|
2645
|
+
return;
|
|
2646
|
+
if (parts[0] === "party" && parts[1]) {
|
|
2647
|
+
checkParty(parts[1], at, inst);
|
|
2648
|
+
if (!found(2))
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (!["self", "input", "party"].includes(parts[0]))
|
|
2652
|
+
parts.unshift("self");
|
|
2653
|
+
let count = 2;
|
|
2654
|
+
while (count <= parts.length && found(count))
|
|
2655
|
+
count++;
|
|
2656
|
+
if (count > parts.length)
|
|
2657
|
+
return;
|
|
2658
|
+
const name = parts[count - 1];
|
|
2659
|
+
if (count > 2) {
|
|
2660
|
+
const prefix = parts.slice(0, count - 1).join(".");
|
|
2661
|
+
const names = members(found(count - 1));
|
|
2662
|
+
fail(at, `\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`, names.length
|
|
2663
|
+
? `Use one of ${names.join(", ")}.`
|
|
2664
|
+
: `\`${prefix}\` has no fields.`);
|
|
2665
|
+
}
|
|
2666
|
+
if (parts[0] === "input")
|
|
2667
|
+
return checkInput(name, at, scope.label, action?.input ?? []);
|
|
2668
|
+
const declared = declarations.get(inst).name;
|
|
2669
|
+
const fields = inst.fields.map((field) => field.name);
|
|
2670
|
+
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(", ")}).`);
|
|
2671
|
+
};
|
|
2672
|
+
const scopeOf = (inst) => ({
|
|
2673
|
+
inst,
|
|
2674
|
+
label: declarations.get(inst).name,
|
|
2675
|
+
});
|
|
2676
|
+
const checkSelection = (selection, at, scope) => {
|
|
2677
|
+
walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
|
|
2678
|
+
walk(selection.where, "where", site(selection, "where", at), scope);
|
|
2679
|
+
for (const id of [selection.instrument].flat()) {
|
|
2680
|
+
checkTarget(id, site(selection, "instrument", at));
|
|
2681
|
+
const target = scopeOf(byId.get(id));
|
|
2682
|
+
const member = (key, where) => checkPath(key.startsWith("self.") ? key : `self.${key}`, where, target);
|
|
2683
|
+
for (const key of Object.keys(selection.where ?? {}))
|
|
2684
|
+
member(key, keySite(selection.where, key, at));
|
|
2685
|
+
member(selection.reference, site(selection, "reference", at));
|
|
2686
|
+
selection.order?.forEach((key, index) => member(key, site(selection.order, index, at)));
|
|
2687
|
+
if (selection.window)
|
|
2688
|
+
member(selection.window.field, site(selection.window, "field", at));
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
const checkCall = (call, at, scope) => {
|
|
2692
|
+
const bind = "range" in call ? call.range?.bind : undefined;
|
|
2693
|
+
for (const [key, value] of Object.entries(call))
|
|
2694
|
+
if (key !== "input")
|
|
2695
|
+
walk(value, key, site(call, key, at), scope);
|
|
2696
|
+
for (const [key, value] of Object.entries(call.input))
|
|
2697
|
+
if (!("field" in value) || value.field !== bind)
|
|
2698
|
+
walk(value, key, site(call.input, key, at), scope);
|
|
2699
|
+
if ("instrument" in call)
|
|
2700
|
+
checkTarget(call.instrument, site(call, "instrument", at));
|
|
2701
|
+
const reference = "reference" in call
|
|
2702
|
+
? resolveField(document, scope.inst, call.reference, scope.action?.input, scope.action)
|
|
2703
|
+
: undefined;
|
|
2704
|
+
const ids = "instrument" in call
|
|
2705
|
+
? [call.instrument]
|
|
2706
|
+
: "selection" in call
|
|
2707
|
+
? [call.selection.instrument].flat()
|
|
2708
|
+
: reference?.type === "ref" && reference.targetKind === "instrument"
|
|
2709
|
+
? [reference.target].flat()
|
|
2710
|
+
: [];
|
|
2711
|
+
for (const id of ids) {
|
|
2712
|
+
const target = byId.get(id);
|
|
2713
|
+
if (!target)
|
|
2714
|
+
continue;
|
|
2715
|
+
const action = target.actions[call.action];
|
|
2716
|
+
if (!action)
|
|
2717
|
+
fail(site(call, "action", at), `\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`, `Use one of ${target.actionOrder.join(", ")}.`);
|
|
2718
|
+
for (const key of Object.keys(call.input))
|
|
2719
|
+
checkInput(key, keySite(call.input, key, at), `${id}.${call.action}`, action.input);
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
const pathKeys = [
|
|
2723
|
+
"field",
|
|
2724
|
+
"reference",
|
|
2725
|
+
"anchor",
|
|
2726
|
+
"at",
|
|
2727
|
+
"list",
|
|
2728
|
+
"from",
|
|
2729
|
+
"to",
|
|
2730
|
+
"transfer",
|
|
2731
|
+
"subject",
|
|
2732
|
+
"instruction",
|
|
2733
|
+
"installment",
|
|
2734
|
+
"recipient",
|
|
2735
|
+
"dueAt",
|
|
2736
|
+
"fields",
|
|
2737
|
+
];
|
|
2738
|
+
const walk = (value, key, at, scope) => {
|
|
2739
|
+
if (typeof value === "string") {
|
|
2740
|
+
if (key === "party")
|
|
2741
|
+
checkParty(value, at, scope.inst);
|
|
2742
|
+
if (key === "target")
|
|
2743
|
+
checkPath(`self.${value}`, at, scope);
|
|
2744
|
+
if (pathKeys.includes(key))
|
|
2745
|
+
checkPath(value, at, scope);
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
if (!value || typeof value !== "object")
|
|
2749
|
+
return;
|
|
2750
|
+
if (Array.isArray(value))
|
|
2751
|
+
return value.forEach((item, index) => walk(item, key, site(value, index, at), scope));
|
|
2752
|
+
if (key === "selection")
|
|
2753
|
+
return checkSelection(value, at, scope);
|
|
2754
|
+
if (key === "invoke")
|
|
2755
|
+
return checkCall(value, at, scope);
|
|
2756
|
+
if (key === "set")
|
|
2757
|
+
for (const name of Object.keys(value))
|
|
2758
|
+
checkPath(`self.${name}`, keySite(value, name, at), scope);
|
|
2759
|
+
for (const [child, item] of Object.entries(value))
|
|
2760
|
+
walk(item, child, site(value, child, at), scope);
|
|
2761
|
+
};
|
|
2762
|
+
for (const inst of document.instruments) {
|
|
2763
|
+
const decl = declarations.get(inst);
|
|
2764
|
+
const scope = scopeOf(inst);
|
|
2765
|
+
for (const field of [
|
|
2766
|
+
...inst.fields,
|
|
2767
|
+
...Object.values(inst.actions).flatMap((action) => action.input),
|
|
2768
|
+
])
|
|
2769
|
+
if ("target" in field && field.target !== undefined)
|
|
2770
|
+
for (const id of [field.target].flat())
|
|
2771
|
+
checkTarget(id, site(field, "target", decl), true);
|
|
2772
|
+
walk(inst.calculate, "calculate", decl, scope);
|
|
2773
|
+
walk(inst.invariants, "invariants", decl, scope);
|
|
2774
|
+
for (const [name, action] of Object.entries(inst.actions))
|
|
2775
|
+
for (const [key, value] of Object.entries(action))
|
|
2776
|
+
if (key !== "input" && key !== "subject")
|
|
2777
|
+
walk(value, key, site(action, key, decl), {
|
|
2778
|
+
inst,
|
|
2779
|
+
label: `${decl.name}.${name}`,
|
|
2780
|
+
action,
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2524
2783
|
const eliminatedStates = new Map();
|
|
2525
2784
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
2526
2785
|
for (const inst of document.instruments) {
|
|
@@ -2628,22 +2887,6 @@ export function compile(source, options = {}) {
|
|
|
2628
2887
|
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");
|
|
2629
2888
|
}
|
|
2630
2889
|
}
|
|
2631
|
-
const usedParties = new Set();
|
|
2632
|
-
const collectParties = (value) => {
|
|
2633
|
-
if (typeof value === "string") {
|
|
2634
|
-
usedParties.add(value);
|
|
2635
|
-
if (value.startsWith("party."))
|
|
2636
|
-
usedParties.add(value.split(".")[1]);
|
|
2637
|
-
}
|
|
2638
|
-
else if (Array.isArray(value))
|
|
2639
|
-
value.forEach(collectParties);
|
|
2640
|
-
else if (value && typeof value === "object")
|
|
2641
|
-
Object.values(value).forEach(collectParties);
|
|
2642
|
-
};
|
|
2643
|
-
collectParties(document.instruments);
|
|
2644
|
-
for (const party of ["programFines", "programCosts"])
|
|
2645
|
-
if (!usedParties.has(party) && !names.has(party))
|
|
2646
|
-
delete document.parties[party];
|
|
2647
2890
|
const validated = validateUdl(document);
|
|
2648
2891
|
if (!validated.ok)
|
|
2649
2892
|
return {
|
|
@@ -2702,7 +2945,7 @@ export function compile(source, options = {}) {
|
|
|
2702
2945
|
if (error instanceof CompileFailure)
|
|
2703
2946
|
return {
|
|
2704
2947
|
verdict: "invalid",
|
|
2705
|
-
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
|
|
2948
|
+
diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
|
|
2706
2949
|
};
|
|
2707
2950
|
throw error;
|
|
2708
2951
|
}
|