@hyperscale0/hsx 2.4.0 → 3.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 +2 -400
- package/README.md +3 -92
- package/dist/src/ast.d.ts +99 -310
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js +16 -69
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.d.ts +6 -19
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +76 -315
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +23 -73
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +1113 -148
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +8 -77
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +13 -422
- package/dist/src/cost.js.map +1 -1
- package/dist/src/format.d.ts +8 -7
- package/dist/src/format.d.ts.map +1 -1
- package/dist/src/format.js +9 -175
- package/dist/src/format.js.map +1 -1
- package/dist/src/headers.d.ts +35 -0
- package/dist/src/headers.d.ts.map +1 -0
- package/dist/src/headers.js +108 -0
- package/dist/src/headers.js.map +1 -0
- package/dist/src/index.d.ts +6 -12
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/lex.d.ts +8 -28
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lex.js +88 -146
- package/dist/src/lex.js.map +1 -1
- package/dist/src/parse.d.ts +4 -17
- package/dist/src/parse.d.ts.map +1 -1
- package/dist/src/parse.js +402 -1040
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +12 -20
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/std-library.d.ts +1 -1
- package/dist/src/std-library.d.ts.map +1 -1
- package/dist/src/std-library.js +1 -5
- package/dist/src/std-library.js.map +1 -1
- package/dist/src/tunables.d.ts +7 -0
- package/dist/src/tunables.d.ts.map +1 -0
- package/dist/src/tunables.js +26 -0
- package/dist/src/tunables.js.map +1 -0
- package/dist/src/version.d.ts +2 -4
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +2 -4
- package/dist/src/version.js.map +1 -1
- package/docs/README.md +179 -41
- package/docs/headers.md +44 -0
- package/examples/cost-table.json +99 -751
- package/examples/library.hsx +59 -0
- package/package.json +9 -17
- package/src/ast.ts +82 -441
- package/src/cli.ts +77 -362
- package/src/compile.ts +1535 -247
- package/src/cost.ts +28 -671
- package/src/format.ts +12 -256
- package/src/headers.ts +126 -0
- package/src/index.ts +7 -39
- package/src/lex.ts +99 -195
- package/src/parse.ts +403 -1209
- package/src/std-bundle.ts +12 -20
- package/src/std-library.ts +2 -7
- package/src/tunables.ts +31 -0
- package/src/version.ts +2 -5
- package/std/approvals.hsx +17 -0
- package/std/cards.hsx +63 -0
- package/std/collections.hsx +31 -0
- package/std/escrow.hsx +43 -0
- package/std/financing.hsx +372 -0
- package/std/insurance.hsx +59 -0
- package/std/lending.hsx +115 -0
- package/std/marketplace.hsx +24 -0
- package/std/money.hsx +195 -0
- package/std/savings.hsx +44 -0
- package/std/travel.hsx +98 -0
- package/std/wallet.hsx +41 -0
- package/dist/src/diagnostics.d.ts +0 -13
- package/dist/src/diagnostics.d.ts.map +0 -1
- package/dist/src/diagnostics.js +0 -587
- package/dist/src/diagnostics.js.map +0 -1
- package/dist/src/emit.d.ts +0 -51
- package/dist/src/emit.d.ts.map +0 -1
- package/dist/src/emit.js +0 -192
- package/dist/src/emit.js.map +0 -1
- package/dist/src/entry-overrides.d.ts +0 -58
- package/dist/src/entry-overrides.d.ts.map +0 -1
- package/dist/src/entry-overrides.js +0 -284
- package/dist/src/entry-overrides.js.map +0 -1
- package/dist/src/ir.d.ts +0 -73
- package/dist/src/ir.d.ts.map +0 -1
- package/dist/src/ir.js +0 -15
- package/dist/src/ir.js.map +0 -1
- package/dist/src/limits.d.ts +0 -23
- package/dist/src/limits.d.ts.map +0 -1
- package/dist/src/limits.js +0 -23
- package/dist/src/limits.js.map +0 -1
- package/dist/src/lsp/server.d.ts +0 -32
- package/dist/src/lsp/server.d.ts.map +0 -1
- package/dist/src/lsp/server.js +0 -391
- package/dist/src/lsp/server.js.map +0 -1
- package/dist/src/modules.d.ts +0 -38
- package/dist/src/modules.d.ts.map +0 -1
- package/dist/src/modules.js +0 -372
- package/dist/src/modules.js.map +0 -1
- package/dist/src/typecheck.d.ts +0 -8
- package/dist/src/typecheck.d.ts.map +0 -1
- package/dist/src/typecheck.js +0 -4131
- package/dist/src/typecheck.js.map +0 -1
- package/docs/guide/01-first-program.md +0 -24
- package/docs/guide/02-money.md +0 -25
- package/docs/guide/03-instruments.md +0 -93
- package/docs/guide/04-lifecycles.md +0 -40
- package/docs/guide/05-fees-and-splits.md +0 -45
- package/docs/guide/06-schedules.md +0 -26
- package/docs/guide/07-composition.md +0 -52
- package/docs/guide/08-writing-a-module.md +0 -85
- package/docs/guide/09-cost.md +0 -24
- package/docs/guide/10-diagnostics.md +0 -27
- package/docs/llms-full.txt +0 -3684
- package/docs/llms.txt +0 -44
- package/docs/piece-plans.md +0 -120
- package/docs/playground.md +0 -55
- package/docs/reference/cli.md +0 -36
- package/docs/reference/diagnostics.md +0 -901
- package/docs/reference/grammar.md +0 -38
- package/docs/reference/std/advance.md +0 -112
- package/docs/reference/std/cancellable_booking.md +0 -159
- package/docs/reference/std/captured_payment.md +0 -115
- package/docs/reference/std/conditional_disbursement.md +0 -101
- package/docs/reference/std/credit_facility.md +0 -108
- package/docs/reference/std/held_payment.md +0 -173
- package/docs/reference/std/instant_transfer.md +0 -97
- package/docs/reference/std/metered.md +0 -89
- package/docs/reference/std/pooled_split.md +0 -95
- package/docs/reference/std/premium_forward.md +0 -119
- package/docs/reference/std/reconciled_payout.md +0 -103
- package/docs/reference/std/rotating_pool.md +0 -130
- package/docs/reference/std/scheduled.md +0 -136
- package/docs/reference/std/security_deposit.md +0 -138
- package/docs/reference/std/settlement_batch.md +0 -123
- package/docs/reference/std/swap.md +0 -133
- package/docs/reference/std/threshold_pool.md +0 -138
- package/docs/reference/std/weighted_distribution.md +0 -135
- package/docs/reference/types.md +0 -61
- package/docs/reference/udl-output.md +0 -13
- package/examples/01-first-program/README.md +0 -45
- package/examples/01-first-program/tip-jar.hsx +0 -17
- package/examples/02-imports-and-modules/README.md +0 -96
- package/examples/02-imports-and-modules/photo-booth.hsx +0 -37
- package/examples/03-diagnostics/README.md +0 -68
- package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
- package/examples/03-diagnostics/corner-shop.hsx +0 -19
- package/examples/04-complete-product/README.md +0 -84
- package/examples/04-complete-product/study-hall.hsx +0 -67
- package/examples/05-authored-instrument/README.md +0 -5
- package/examples/05-authored-instrument/payment.hsx +0 -37
- package/examples/05-watch-club/README.md +0 -6
- package/examples/05-watch-club/watch-club.hsx +0 -123
- package/examples/README.md +0 -48
- package/examples/advance/README.md +0 -3
- package/examples/advance/advance.hsx +0 -13
- package/examples/advance/advance.udl +0 -367
- package/examples/cancellable_booking/README.md +0 -3
- package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
- package/examples/cancellable_booking/cancellable_booking.udl +0 -413
- package/examples/captured_payment/README.md +0 -3
- package/examples/captured_payment/captured_payment.hsx +0 -17
- package/examples/captured_payment/captured_payment.udl +0 -686
- package/examples/conditional_disbursement/README.md +0 -3
- package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -15
- package/examples/conditional_disbursement/conditional_disbursement.udl +0 -418
- package/examples/credit_facility/README.md +0 -3
- package/examples/credit_facility/credit_facility.hsx +0 -24
- package/examples/credit_facility/credit_facility.udl +0 -1396
- package/examples/held_payment/README.md +0 -3
- package/examples/held_payment/held_payment.hsx +0 -13
- package/examples/held_payment/held_payment.udl +0 -569
- package/examples/instant_transfer/README.md +0 -3
- package/examples/instant_transfer/instant_transfer.hsx +0 -16
- package/examples/instant_transfer/instant_transfer.udl +0 -376
- package/examples/metered/README.md +0 -3
- package/examples/metered/metered.hsx +0 -13
- package/examples/metered/metered.udl +0 -253
- package/examples/pooled_split/README.md +0 -3
- package/examples/pooled_split/pooled_split.hsx +0 -15
- package/examples/pooled_split/pooled_split.udl +0 -432
- package/examples/premium_forward/README.md +0 -3
- package/examples/premium_forward/premium_forward.hsx +0 -19
- package/examples/premium_forward/premium_forward.udl +0 -604
- package/examples/reconciled_payout/README.md +0 -3
- package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
- package/examples/reconciled_payout/reconciled_payout.udl +0 -312
- package/examples/rotating_pool/README.md +0 -3
- package/examples/rotating_pool/rotating_pool.hsx +0 -18
- package/examples/rotating_pool/rotating_pool.udl +0 -4370
- package/examples/scheduled/README.md +0 -3
- package/examples/scheduled/scheduled.hsx +0 -12
- package/examples/scheduled/scheduled.udl +0 -355
- package/examples/security_deposit/README.md +0 -3
- package/examples/security_deposit/security_deposit.hsx +0 -21
- package/examples/security_deposit/security_deposit.udl +0 -320
- package/examples/settlement_batch/README.md +0 -3
- package/examples/settlement_batch/settlement_batch.hsx +0 -19
- package/examples/settlement_batch/settlement_batch.udl +0 -757
- package/examples/swap/README.md +0 -3
- package/examples/swap/swap.hsx +0 -19
- package/examples/swap/swap.udl +0 -892
- package/examples/threshold_pool/README.md +0 -3
- package/examples/threshold_pool/threshold_pool.hsx +0 -16
- package/examples/threshold_pool/threshold_pool.udl +0 -680
- package/examples/weighted_distribution/README.md +0 -3
- package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
- package/examples/weighted_distribution/weighted_distribution.udl +0 -348
- package/skills/hsx/SKILL.md +0 -449
- package/src/diagnostics.ts +0 -620
- package/src/emit.ts +0 -282
- package/src/entry-overrides.ts +0 -428
- package/src/ir.ts +0 -113
- package/src/limits.ts +0 -22
- package/src/lsp/server.ts +0 -460
- package/src/modules.ts +0 -452
- package/src/typecheck.ts +0 -5381
- package/std/SEMANTICS.md +0 -44
- package/std/money_flows/advance.hsx +0 -235
- package/std/money_flows/cancellable_booking.hsx +0 -402
- package/std/money_flows/captured_payment.hsx +0 -220
- package/std/money_flows/conditional_disbursement.hsx +0 -226
- package/std/money_flows/credit_facility.hsx +0 -214
- package/std/money_flows/held_payment.hsx +0 -812
- package/std/money_flows/index.hsx +0 -4
- package/std/money_flows/instant_transfer.hsx +0 -231
- package/std/money_flows/metered.hsx +0 -104
- package/std/money_flows/pooled_split.hsx +0 -109
- package/std/money_flows/premium_forward.hsx +0 -307
- package/std/money_flows/reconciled_payout.hsx +0 -119
- package/std/money_flows/rotating_pool.hsx +0 -305
- package/std/money_flows/scheduled.hsx +0 -599
- package/std/money_flows/security_deposit.hsx +0 -248
- package/std/money_flows/settlement_batch.hsx +0 -173
- package/std/money_flows/swap.hsx +0 -1192
- package/std/money_flows/threshold_pool.hsx +0 -307
- package/std/money_flows/weighted_distribution.hsx +0 -237
package/dist/src/compile.js
CHANGED
|
@@ -1,163 +1,1128 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* - `valid`: the program lowers cleanly; the IR document is ready
|
|
5
|
-
* for the independent checker.
|
|
6
|
-
* - `warning`: the program lowers, and the compiler's lint voice has notes
|
|
7
|
-
* the author should read (the artifacts are still present and usable).
|
|
8
|
-
* - `invalid`: the program cannot be lowered; diagnostics say why, in the
|
|
9
|
-
* author's language, each anchored to a source line and column.
|
|
10
|
-
*
|
|
11
|
-
* Every diagnostic points at SOURCE coordinates, never at lowered IR paths.
|
|
12
|
-
*/
|
|
13
|
-
import { lineColIn, lineIndex } from "./ast.js";
|
|
14
|
-
import { buildCostManifest, } from "./cost.js";
|
|
15
|
-
import { lowerGeneralProgram } from "./emit.js";
|
|
16
|
-
import { resolveProgramModules, } from "./modules.js";
|
|
1
|
+
import { buildUdlCostManifest } from "./cost.js";
|
|
2
|
+
import { validateUdl, } from "@hyperscale0/udl";
|
|
3
|
+
import { tunableBounds } from "./tunables.js";
|
|
17
4
|
import { parseProgram } from "./parse.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const byteOffsets = utf8OffsetIndex(source);
|
|
26
|
-
const at = (stage, severity, message, span, details, origin) => {
|
|
27
|
-
const position = origin ?? lineColIn(lines, span.start);
|
|
28
|
-
diagnostics.push({
|
|
29
|
-
column: position.column,
|
|
30
|
-
...(origin
|
|
31
|
-
? { file: origin.moduleName }
|
|
32
|
-
: {
|
|
33
|
-
byteSpan: {
|
|
34
|
-
end: byteOffsets[span.end] ?? byteOffsets[byteOffsets.length - 1],
|
|
35
|
-
start: byteOffsets[span.start] ?? 0,
|
|
36
|
-
},
|
|
37
|
-
}),
|
|
38
|
-
line: position.line,
|
|
39
|
-
message,
|
|
40
|
-
span: { ...span },
|
|
41
|
-
...(details ? details : {}),
|
|
42
|
-
severity,
|
|
43
|
-
stage,
|
|
44
|
-
});
|
|
45
|
-
};
|
|
46
|
-
const optionKeys = new Set([
|
|
47
|
-
"costTable",
|
|
48
|
-
"moduleName",
|
|
49
|
-
"publishedCatalog",
|
|
50
|
-
"resolveModule",
|
|
51
|
-
"standardLibrary",
|
|
52
|
-
]);
|
|
53
|
-
for (const key of Object.keys(options)) {
|
|
54
|
-
if (optionKeys.has(key))
|
|
55
|
-
continue;
|
|
56
|
-
at("bind", "error", `unknown compile option ${key}`, { start: 0, end: 0 });
|
|
57
|
-
}
|
|
58
|
-
if (diagnostics.length > 0)
|
|
59
|
-
return { diagnostics, verdict: "invalid" };
|
|
60
|
-
const parsed = parseProgram(source);
|
|
61
|
-
for (const diagnostic of parsed.diagnostics) {
|
|
62
|
-
at("parse", "error", diagnostic.message, diagnostic.span, diagnostic.code && diagnostic.fix
|
|
63
|
-
? { code: diagnostic.code, fix: diagnostic.fix }
|
|
64
|
-
: undefined);
|
|
65
|
-
}
|
|
66
|
-
if (parsed.diagnostics.length > 0) {
|
|
67
|
-
return { diagnostics, verdict: "invalid" };
|
|
5
|
+
import { lineColAt, } from "./ast.js";
|
|
6
|
+
import { bundledStandardLibrary } from "./std-library.js";
|
|
7
|
+
class CompileFailure extends Error {
|
|
8
|
+
diagnostic;
|
|
9
|
+
constructor(diagnostic) {
|
|
10
|
+
super(diagnostic.message);
|
|
11
|
+
this.diagnostic = diagnostic;
|
|
68
12
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
13
|
+
}
|
|
14
|
+
function fail(expr, message, fix) {
|
|
15
|
+
throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
|
|
16
|
+
}
|
|
17
|
+
const emptyBlock = {
|
|
18
|
+
kind: "block",
|
|
19
|
+
entries: [],
|
|
20
|
+
span: { start: 0, end: 0 },
|
|
21
|
+
};
|
|
22
|
+
const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
23
|
+
/** Clock and parent actors run without a caller, so they have no public name. */
|
|
24
|
+
const automatic = (actor) => actor === "clock" || (typeof actor === "object" && "parent" in actor);
|
|
25
|
+
const title = (name) => name[0].toUpperCase() + name.slice(1).replaceAll("_", " ");
|
|
26
|
+
function entries(block) {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const row of block.entries) {
|
|
29
|
+
const previous = map.get(row.key);
|
|
30
|
+
if (previous) {
|
|
31
|
+
if (!["requires", "moves", "invariants", "invoke", "calculate"].includes(row.key))
|
|
32
|
+
fail(row, `duplicate ${row.key}`, "keep one value for this name");
|
|
33
|
+
const items = (expr) => expr.kind === "list" ? expr.items : [expr];
|
|
34
|
+
map.set(row.key, {
|
|
35
|
+
kind: "list",
|
|
36
|
+
items: [...items(previous), ...items(row.value)],
|
|
37
|
+
span: row.span,
|
|
75
38
|
});
|
|
76
39
|
}
|
|
77
|
-
|
|
40
|
+
else
|
|
41
|
+
map.set(row.key, row.value);
|
|
78
42
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
function asBlock(expr) {
|
|
46
|
+
if (!expr)
|
|
47
|
+
return emptyBlock;
|
|
48
|
+
if (expr.kind !== "block")
|
|
49
|
+
fail(expr, "expected a block", "write { name: value }");
|
|
50
|
+
return expr;
|
|
51
|
+
}
|
|
52
|
+
function text(expr) {
|
|
53
|
+
if ("value" in expr && typeof expr.value === "string")
|
|
54
|
+
return expr.value;
|
|
55
|
+
return fail(expr, "expected a name or literal", "write a single name or literal");
|
|
56
|
+
}
|
|
57
|
+
function decimal(raw, scale, expr) {
|
|
58
|
+
const [whole = "", fraction = ""] = raw.split(".");
|
|
59
|
+
if (!/^\d+$/.test(whole) || fraction.length > scale)
|
|
60
|
+
fail(expr, `literal has more than ${scale} decimal places`, `write at most ${scale} decimal places`);
|
|
61
|
+
return (BigInt(whole) * 10n ** BigInt(scale) +
|
|
62
|
+
BigInt(fraction.padEnd(scale, "0") || "0")).toString();
|
|
63
|
+
}
|
|
64
|
+
function literal(expr) {
|
|
65
|
+
if (expr.kind === "money") {
|
|
66
|
+
const [raw, currency] = expr.value.split(" ");
|
|
67
|
+
if (currency !== "SAR")
|
|
68
|
+
fail(expr, `currency ${currency} differs from SAR`, "write this amount in SAR");
|
|
69
|
+
const result = decimal(raw, 2, expr);
|
|
70
|
+
if (result.length > 18)
|
|
71
|
+
fail(expr, "amount exceeds 18 minor-unit digits", "write a smaller amount");
|
|
72
|
+
return result;
|
|
90
73
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
at("typecheck", diagnostic.severity, diagnostic.message, diagnostic.span, {
|
|
97
|
-
code: diagnostic.code,
|
|
98
|
-
fix: diagnostic.fix,
|
|
99
|
-
...(diagnostic.path ? { path: diagnostic.path } : {}),
|
|
100
|
-
...(diagnostic.udlCode ? { udlCode: diagnostic.udlCode } : {}),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return { diagnostics, verdict: "invalid" };
|
|
74
|
+
if (expr.kind === "percent") {
|
|
75
|
+
const n = Number(decimal(expr.value, 2, expr));
|
|
76
|
+
if (n > 10000)
|
|
77
|
+
fail(expr, "percentage exceeds 100%", "write a percentage from 0% to 100%");
|
|
78
|
+
return n;
|
|
104
79
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
80
|
+
if (expr.kind === "number") {
|
|
81
|
+
const n = Number(expr.value);
|
|
82
|
+
if (!Number.isSafeInteger(n))
|
|
83
|
+
fail(expr, "expected a safe integer", "write an integer, a percentage, or an amount with SAR");
|
|
84
|
+
return n;
|
|
85
|
+
}
|
|
86
|
+
if (expr.kind === "duration" ||
|
|
87
|
+
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)) ||
|
|
88
|
+
(expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))) {
|
|
89
|
+
const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
|
|
90
|
+
const units = {
|
|
91
|
+
ms: 1,
|
|
92
|
+
s: 1000,
|
|
93
|
+
m: 60000,
|
|
94
|
+
h: 3600000,
|
|
95
|
+
d: 86400000,
|
|
96
|
+
w: 604800000,
|
|
97
|
+
};
|
|
98
|
+
const iso = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(expr.value);
|
|
99
|
+
const n = short
|
|
100
|
+
? Number(short[1]) * units[short[2]]
|
|
101
|
+
: iso
|
|
102
|
+
? Number(iso[1] ?? 0) * units.w +
|
|
103
|
+
Number(iso[2] ?? 0) * units.d +
|
|
104
|
+
Number(iso[3] ?? 0) * units.h +
|
|
105
|
+
Number(iso[4] ?? 0) * units.m +
|
|
106
|
+
Number(iso[5] ?? 0) * units.s
|
|
107
|
+
: NaN;
|
|
108
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
109
|
+
fail(expr, "invalid duration", "write a positive fixed duration such as 48h or 3d");
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
if (expr.kind === "date") {
|
|
113
|
+
const raw = expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
|
|
114
|
+
const n = Date.parse(raw);
|
|
115
|
+
if (!Number.isFinite(n) ||
|
|
116
|
+
new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
|
|
117
|
+
raw.slice(0, 10))
|
|
118
|
+
fail(expr, "invalid date", "write a calendar date or a timestamp with an explicit offset");
|
|
119
|
+
return new Date(n).toISOString();
|
|
116
120
|
}
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
if (expr.kind === "text")
|
|
122
|
+
return expr.value;
|
|
123
|
+
if (expr.kind === "name" && ["true", "false"].includes(expr.value))
|
|
124
|
+
return expr.value === "true";
|
|
125
|
+
return fail(expr, "expected a literal", "write a typed constant");
|
|
126
|
+
}
|
|
127
|
+
export function compile(source, options = {}) {
|
|
128
|
+
const parsed = parseProgram(source);
|
|
129
|
+
const diagnostic = (d, stage) => ({
|
|
130
|
+
...d,
|
|
131
|
+
...lineColAt(source, d.span.start),
|
|
132
|
+
severity: "error",
|
|
133
|
+
stage,
|
|
134
|
+
});
|
|
135
|
+
if (parsed.diagnostics.length)
|
|
119
136
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
verdict: "invalid",
|
|
138
|
+
diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
|
|
139
|
+
};
|
|
140
|
+
const program = parsed.program;
|
|
141
|
+
try {
|
|
142
|
+
if (program.header)
|
|
143
|
+
fail(program, "compile a program, not a header", 'start with program product_name "Title"');
|
|
144
|
+
if (program.currency !== "SAR")
|
|
145
|
+
fail(program, "this release supports SAR", "write currency SAR or omit currency");
|
|
146
|
+
const templates = new Map();
|
|
147
|
+
const used = new Set();
|
|
148
|
+
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
149
|
+
if (used.has(use.name))
|
|
150
|
+
fail(use, `header ${use.name} is used twice`, "remove the duplicate use line");
|
|
151
|
+
used.add(use.name);
|
|
152
|
+
const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
|
|
153
|
+
if (!content)
|
|
154
|
+
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
155
|
+
const header = parseProgram(content);
|
|
156
|
+
if (header.diagnostics.length ||
|
|
157
|
+
!header.program.header ||
|
|
158
|
+
header.program.name !== use.name)
|
|
159
|
+
fail(use, `header ${use.name} is malformed`, "repair the header source before compiling");
|
|
160
|
+
for (const decl of header.program.decls)
|
|
161
|
+
if (decl.kind === "instrument")
|
|
162
|
+
templates.set(`${use.name}.${decl.name}`, decl);
|
|
163
|
+
}
|
|
164
|
+
const document = {
|
|
165
|
+
udl: 3,
|
|
166
|
+
version: 1,
|
|
167
|
+
product: program.name,
|
|
168
|
+
title: program.title,
|
|
169
|
+
currency: "SAR",
|
|
170
|
+
parties: {
|
|
171
|
+
programOperator: { kind: "business", role: "program_operator" },
|
|
172
|
+
programTax: { kind: "business", role: "tax_payable" },
|
|
173
|
+
programFines: { kind: "business", role: "fine_payable" },
|
|
174
|
+
programCosts: { kind: "business", role: "cost_recovery" },
|
|
126
175
|
},
|
|
176
|
+
instruments: [],
|
|
127
177
|
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
offsets[index] = bytes;
|
|
147
|
-
const codePoint = source.codePointAt(index);
|
|
148
|
-
if (codePoint > 0xffff) {
|
|
149
|
-
offsets[index + 1] = bytes;
|
|
150
|
-
bytes += 4;
|
|
151
|
-
index += 1;
|
|
178
|
+
const objects = new Map();
|
|
179
|
+
const names = new Set();
|
|
180
|
+
for (const decl of program.decls) {
|
|
181
|
+
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
182
|
+
continue;
|
|
183
|
+
if (names.has(decl.name))
|
|
184
|
+
fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
|
|
185
|
+
names.add(decl.name);
|
|
186
|
+
if (decl.kind === "object")
|
|
187
|
+
objects.set(decl.name, decl);
|
|
188
|
+
if (decl.kind === "party") {
|
|
189
|
+
if (!["person", "business", "staff"].includes(decl.partyKind))
|
|
190
|
+
fail(decl, `unknown party kind ${decl.partyKind}`, "choose person, business, or staff");
|
|
191
|
+
document.parties[decl.name] = {
|
|
192
|
+
kind: decl.partyKind,
|
|
193
|
+
...(decl.role ? { role: decl.role } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
152
196
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
const origins = [];
|
|
198
|
+
const materialApprovals = new Set();
|
|
199
|
+
const implicitDecisions = new Map();
|
|
200
|
+
const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedApprovers = new Set(), inheritedEnums = new Map()) => {
|
|
201
|
+
const enums = new Map(inheritedEnums);
|
|
202
|
+
for (const parameter of decl.parameters) {
|
|
203
|
+
const type = parameter.value.kind === "default"
|
|
204
|
+
? parameter.value.type
|
|
205
|
+
: parameter.value;
|
|
206
|
+
if (type.kind === "call" && type.name === "enum")
|
|
207
|
+
enums.set(parameter.key, type.args.map(text));
|
|
208
|
+
}
|
|
209
|
+
const approvers = new Set(inheritedApprovers);
|
|
210
|
+
const supplied = entries(arguments_);
|
|
211
|
+
const environment = new Map(inherited);
|
|
212
|
+
for (const param of decl.parameters) {
|
|
213
|
+
const type = param.value.kind === "default" ? param.value.type : param.value;
|
|
214
|
+
const fallback = param.value.kind === "default" ? param.value.value : undefined;
|
|
215
|
+
const actual = supplied.get(param.key) ?? fallback;
|
|
216
|
+
if (!actual) {
|
|
217
|
+
if (type.kind === "type" && type.optional)
|
|
218
|
+
continue;
|
|
219
|
+
fail({ span: origin }, `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
|
|
220
|
+
}
|
|
221
|
+
environment.set(param.key, actual);
|
|
222
|
+
}
|
|
223
|
+
for (const key of supplied.keys())
|
|
224
|
+
if (!decl.parameters.some((p) => p.key === key))
|
|
225
|
+
fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
|
|
226
|
+
const resolve = (expr, seen = new Set()) => {
|
|
227
|
+
if (expr.kind === "call" &&
|
|
228
|
+
["object", "all", "party"].includes(expr.name)) {
|
|
229
|
+
if (expr.args.length !== 1)
|
|
230
|
+
fail(expr, `${expr.name} needs one type`, "supply one declared object or party kind");
|
|
231
|
+
const type = text(expr.args[0]);
|
|
232
|
+
const matches = expr.name === "party"
|
|
233
|
+
? Object.entries(document.parties)
|
|
234
|
+
.filter(([, party]) => party.kind === type)
|
|
235
|
+
.map(([name]) => name)
|
|
236
|
+
: [...objects.values()]
|
|
237
|
+
.filter((object) => object.object === type ||
|
|
238
|
+
type.startsWith(`${object.object}.`))
|
|
239
|
+
.map((object) => object.name +
|
|
240
|
+
type.slice(object.object.length).replaceAll(".", "_"));
|
|
241
|
+
if (expr.name !== "all" && matches.length !== 1)
|
|
242
|
+
fail({ span: origin }, `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
|
|
243
|
+
const items = matches.map((value) => ({
|
|
244
|
+
kind: "name",
|
|
245
|
+
value,
|
|
246
|
+
span: origin,
|
|
247
|
+
}));
|
|
248
|
+
return expr.name === "all"
|
|
249
|
+
? { kind: "list", items, span: origin }
|
|
250
|
+
: items[0];
|
|
251
|
+
}
|
|
252
|
+
if (expr.kind !== "name")
|
|
253
|
+
return expr;
|
|
254
|
+
if (expr.value.startsWith("party.")) {
|
|
255
|
+
const binding = environment.get(expr.value.slice(6));
|
|
256
|
+
if (binding?.kind === "name" && document.parties[binding.value])
|
|
257
|
+
return { ...expr, value: `party.${binding.value}` };
|
|
258
|
+
}
|
|
259
|
+
const [root, ...tail] = expr.value.split(".");
|
|
260
|
+
const bound = environment.get(root);
|
|
261
|
+
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
262
|
+
return expr;
|
|
263
|
+
if (seen.has(root))
|
|
264
|
+
return fail(expr, `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
|
|
265
|
+
let resolved = supplied.has(root) || inherited.has(root) || enums.has(root)
|
|
266
|
+
? bound
|
|
267
|
+
: resolve(bound, new Set([...seen, root]));
|
|
268
|
+
for (const key of tail) {
|
|
269
|
+
if (resolved.kind !== "block")
|
|
270
|
+
return expr;
|
|
271
|
+
const child = entries(resolved).get(key);
|
|
272
|
+
if (!child)
|
|
273
|
+
return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
|
|
274
|
+
resolved = resolve(child, new Set([...seen, root]));
|
|
275
|
+
}
|
|
276
|
+
return resolved;
|
|
277
|
+
};
|
|
278
|
+
for (const param of decl.parameters) {
|
|
279
|
+
const actual = environment.get(param.key);
|
|
280
|
+
if (!actual)
|
|
281
|
+
continue;
|
|
282
|
+
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
283
|
+
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
284
|
+
const v = supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
|
|
285
|
+
environment.set(param.key, v);
|
|
286
|
+
if (type === "enum" && t.kind === "call") {
|
|
287
|
+
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
288
|
+
fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
|
|
289
|
+
}
|
|
290
|
+
else if (type === "list") {
|
|
291
|
+
if (v.kind !== "list")
|
|
292
|
+
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
293
|
+
}
|
|
294
|
+
else if (type === "party" || type === "approval") {
|
|
295
|
+
if (v.kind !== "name" || !document.parties[v.value])
|
|
296
|
+
fail(v, `${param.key} needs a declared party`, "declare a party and use its name here");
|
|
297
|
+
if (type === "approval")
|
|
298
|
+
approvers.add(text(v));
|
|
299
|
+
}
|
|
300
|
+
else if (type === "ref") {
|
|
301
|
+
const values = v.kind === "list" ? v.items : [v];
|
|
302
|
+
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
303
|
+
fail(v, `${param.key} accepts one reference`, "use one object name");
|
|
304
|
+
if (!values.length || values.length > 16)
|
|
305
|
+
fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
for (const value of values) {
|
|
308
|
+
if (value.kind !== "name")
|
|
309
|
+
fail(value, "reference needs an object name", "name a declared object");
|
|
310
|
+
const [root, ...tail] = value.value.split(".");
|
|
311
|
+
const obj = objects.get(root);
|
|
312
|
+
if ((!obj &&
|
|
313
|
+
!document.instruments.some((inst) => inst.id === value.value)) ||
|
|
314
|
+
(t.kind === "type" &&
|
|
315
|
+
t.target &&
|
|
316
|
+
[obj?.object, ...tail].join(".") !== t.target))
|
|
317
|
+
fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
|
|
318
|
+
if (seen.has(value.value))
|
|
319
|
+
fail(value, "duplicate reference", "list each object once");
|
|
320
|
+
seen.add(value.value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (type === "fee" || type === "split" || type === "policy") {
|
|
324
|
+
if (v.kind !== "block")
|
|
325
|
+
fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
|
|
326
|
+
}
|
|
327
|
+
else if (type === "money" ||
|
|
328
|
+
type === "percent" ||
|
|
329
|
+
type === "date" ||
|
|
330
|
+
type === "duration" ||
|
|
331
|
+
type === "integer" ||
|
|
332
|
+
type === "text") {
|
|
333
|
+
if (v.kind === "name" && v.value === "runtime")
|
|
334
|
+
continue;
|
|
335
|
+
const expected = type === "integer" ? "number" : type;
|
|
336
|
+
if (v.kind !== expected &&
|
|
337
|
+
!(type === "duration" &&
|
|
338
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
339
|
+
/^P/.test(v.value)))
|
|
340
|
+
fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
|
|
341
|
+
try {
|
|
342
|
+
const value = literal(v);
|
|
343
|
+
const bounds = tunableBounds(t);
|
|
344
|
+
if (bounds &&
|
|
345
|
+
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
346
|
+
BigInt(String(value)) > BigInt(bounds.maximum)))
|
|
347
|
+
fail(v, `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`, "choose a value inside the tunable's bounds");
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof CompileFailure)
|
|
351
|
+
fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const body = entries(decl.body);
|
|
357
|
+
for (const constraint of asBlock(body.get("constraints")).entries) {
|
|
358
|
+
const rule = constraint.value;
|
|
359
|
+
if (rule.kind !== "call" ||
|
|
360
|
+
rule.args.length !== 1 ||
|
|
361
|
+
!["less_than", "at_most", "greater_than"].includes(rule.name))
|
|
362
|
+
fail(rule, "invalid tunable constraint", "use less_than(name), at_most(name), or greater_than(name)");
|
|
363
|
+
const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
|
|
364
|
+
if (!left || !right)
|
|
365
|
+
fail(rule, "constraint needs two declared tunables", "name the compared tunables");
|
|
366
|
+
const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
|
|
367
|
+
if (!(rule.name === "less_than"
|
|
368
|
+
? a < b
|
|
369
|
+
: rule.name === "at_most"
|
|
370
|
+
? a <= b
|
|
371
|
+
: a > b))
|
|
372
|
+
fail(left, `${constraint.key} must be ${rule.name} ${text(rule.args[0])}`, "choose values satisfying the header constraint");
|
|
373
|
+
}
|
|
374
|
+
for (const key of body.keys())
|
|
375
|
+
if (![
|
|
376
|
+
"fields",
|
|
377
|
+
"lifecycle",
|
|
378
|
+
"records",
|
|
379
|
+
"summary",
|
|
380
|
+
"invariants",
|
|
381
|
+
"constraints",
|
|
382
|
+
].includes(key) &&
|
|
383
|
+
!key.startsWith("action "))
|
|
384
|
+
fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
|
|
385
|
+
const records = entries(asBlock(body.get("records")));
|
|
386
|
+
if (!inherited.size)
|
|
387
|
+
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
388
|
+
for (const key of records.keys())
|
|
389
|
+
if (!decl.parameters.some((parameter) => parameter.key === key))
|
|
390
|
+
environment.set(key, {
|
|
391
|
+
kind: "name",
|
|
392
|
+
value: `${id}_${key}`,
|
|
393
|
+
span: origin,
|
|
394
|
+
});
|
|
395
|
+
const fields = [];
|
|
396
|
+
const calculations = [];
|
|
397
|
+
const path = (expr) => {
|
|
398
|
+
const value = resolve(expr);
|
|
399
|
+
const name = text(value);
|
|
400
|
+
if (document.parties[name])
|
|
401
|
+
return `party.${name}`;
|
|
402
|
+
return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
|
|
403
|
+
};
|
|
404
|
+
const val = (expr) => {
|
|
405
|
+
const v = resolve(expr);
|
|
406
|
+
return v.kind === "name" && !["true", "false"].includes(v.value)
|
|
407
|
+
? { field: path(v) }
|
|
408
|
+
: { literal: literal(v) };
|
|
409
|
+
};
|
|
410
|
+
const data = (expr) => {
|
|
411
|
+
if (expr.kind === "call" &&
|
|
412
|
+
expr.name === "current" &&
|
|
413
|
+
!expr.args.length)
|
|
414
|
+
return id;
|
|
415
|
+
const value = resolve(expr);
|
|
416
|
+
if (value.kind === "block") {
|
|
417
|
+
const result = Object.fromEntries([...entries(value)].map(([key, value]) => [key, data(value)]));
|
|
418
|
+
const selection = result.selection;
|
|
419
|
+
if (Array.isArray(selection?.instrument) &&
|
|
420
|
+
selection.instrument.length === 0) {
|
|
421
|
+
if (result.op === "aggregate")
|
|
422
|
+
return {
|
|
423
|
+
target: result.target,
|
|
424
|
+
op: "sum",
|
|
425
|
+
values: [{ literal: result.measure === "count" ? 0 : "0" }],
|
|
426
|
+
};
|
|
427
|
+
if (result.kind === "aggregate")
|
|
428
|
+
return {
|
|
429
|
+
kind: "compare",
|
|
430
|
+
left: { literal: result.measure === "count" ? 0 : "0" },
|
|
431
|
+
operator: result.operator,
|
|
432
|
+
right: result.value,
|
|
433
|
+
};
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
if (value.kind === "list")
|
|
439
|
+
return value.items.map(data).filter((item) => item !== null);
|
|
440
|
+
if (value.kind === "call" &&
|
|
441
|
+
value.name === "child" &&
|
|
442
|
+
value.args.length === 2) {
|
|
443
|
+
const parent = resolve(value.args[0]);
|
|
444
|
+
const child = (item) => `${text(item).replaceAll(".", "_")}_${text(value.args[1])}`;
|
|
445
|
+
return parent.kind === "list"
|
|
446
|
+
? parent.items.map(child)
|
|
447
|
+
: child(parent);
|
|
448
|
+
}
|
|
449
|
+
if (value.kind === "name")
|
|
450
|
+
return value.value === "true" || value.value === "false"
|
|
451
|
+
? value.value === "true"
|
|
452
|
+
: value.value;
|
|
453
|
+
return literal(value);
|
|
454
|
+
};
|
|
455
|
+
const lowerFields = (block) => {
|
|
456
|
+
const result = [];
|
|
457
|
+
for (const row of block.entries) {
|
|
458
|
+
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
459
|
+
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
460
|
+
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
461
|
+
const f = {
|
|
462
|
+
name: row.key,
|
|
463
|
+
type,
|
|
464
|
+
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
465
|
+
};
|
|
466
|
+
if (type === "enum" && t.kind === "call")
|
|
467
|
+
f.values = t.args.map(text);
|
|
468
|
+
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
469
|
+
if (t.args.length !== 2)
|
|
470
|
+
fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
|
|
471
|
+
f.minimum = literal(resolve(t.args[0]));
|
|
472
|
+
f.maximum = literal(resolve(t.args[1]));
|
|
473
|
+
}
|
|
474
|
+
if (type === "list" && t.kind === "call") {
|
|
475
|
+
f.item = text(t.args[0]);
|
|
476
|
+
f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
|
|
477
|
+
}
|
|
478
|
+
if (type === "list" && t.kind === "type") {
|
|
479
|
+
f.item = t.target;
|
|
480
|
+
f.maxItems = 366;
|
|
481
|
+
}
|
|
482
|
+
if (type === "ref") {
|
|
483
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
484
|
+
if (!target)
|
|
485
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
486
|
+
const [root, ...tail] = target.split(".");
|
|
487
|
+
const resolved = environment.get(root);
|
|
488
|
+
const resolvedTargets = resolved?.kind === "list"
|
|
489
|
+
? resolved.items
|
|
490
|
+
: resolved
|
|
491
|
+
? [resolved]
|
|
492
|
+
: [];
|
|
493
|
+
const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
|
|
494
|
+
f.target =
|
|
495
|
+
target === "self"
|
|
496
|
+
? id
|
|
497
|
+
: targets.length === 1
|
|
498
|
+
? targets[0]
|
|
499
|
+
: targets.length
|
|
500
|
+
? targets
|
|
501
|
+
: target;
|
|
502
|
+
}
|
|
503
|
+
else if (type === "account") {
|
|
504
|
+
if (t.kind === "call") {
|
|
505
|
+
if (t.args.length < 1 || t.args.length > 4)
|
|
506
|
+
fail(t, "account needs an owner, optional book, mode and key", "write account(buyer, claim, contra)");
|
|
507
|
+
f.owner = text(resolve(t.args[0]));
|
|
508
|
+
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
509
|
+
if (t.args[2]) {
|
|
510
|
+
const mode = text(t.args[2]);
|
|
511
|
+
if (["contra", "external"].includes(mode))
|
|
512
|
+
f[mode] = true;
|
|
513
|
+
else
|
|
514
|
+
f.key = mode;
|
|
515
|
+
}
|
|
516
|
+
if (t.args[3])
|
|
517
|
+
f.key = text(t.args[3]);
|
|
518
|
+
}
|
|
519
|
+
else if (t.kind === "type" && t.owner) {
|
|
520
|
+
f.owner =
|
|
521
|
+
t.owner === "self"
|
|
522
|
+
? "self"
|
|
523
|
+
: text(resolve({ kind: "name", value: t.owner, span: row.span }));
|
|
524
|
+
f.book = "cash";
|
|
525
|
+
}
|
|
526
|
+
else
|
|
527
|
+
fail(row, "account needs an owner", "write account of buyer or account of self");
|
|
528
|
+
}
|
|
529
|
+
if (constant &&
|
|
530
|
+
!(constant.kind === "name" && constant.value === "runtime")) {
|
|
531
|
+
if (constant.kind === "call") {
|
|
532
|
+
const args = constant.name === "aggregate" ? [] : constant.args.map(val);
|
|
533
|
+
const target = row.key;
|
|
534
|
+
if (constant.name === "aggregate" && constant.args.length === 2)
|
|
535
|
+
calculations.push({
|
|
536
|
+
target,
|
|
537
|
+
op: "aggregate",
|
|
538
|
+
selection: data(constant.args[0]),
|
|
539
|
+
measure: text(constant.args[1]) === "count"
|
|
540
|
+
? "count"
|
|
541
|
+
: { sum: text(constant.args[1]) },
|
|
542
|
+
});
|
|
543
|
+
else if (constant.name === "at" && args.length === 2)
|
|
544
|
+
calculations.push({
|
|
545
|
+
target,
|
|
546
|
+
op: "at",
|
|
547
|
+
list: path(constant.args[0]),
|
|
548
|
+
position: args[1],
|
|
549
|
+
});
|
|
550
|
+
else if (constant.name === "ratio" && args.length === 3)
|
|
551
|
+
calculations.push({
|
|
552
|
+
target,
|
|
553
|
+
op: "ratio",
|
|
554
|
+
amount: args[0],
|
|
555
|
+
numerator: args[1],
|
|
556
|
+
denominator: args[2],
|
|
557
|
+
rounding: "floor",
|
|
558
|
+
});
|
|
559
|
+
else if (constant.name === "rate" && args.length === 2)
|
|
560
|
+
calculations.push({
|
|
561
|
+
target,
|
|
562
|
+
op: "rate",
|
|
563
|
+
base: args[0],
|
|
564
|
+
bps: args[1],
|
|
565
|
+
rounding: "floor",
|
|
566
|
+
});
|
|
567
|
+
else if (constant.name === "sum" || constant.name === "minimum")
|
|
568
|
+
calculations.push({ target, op: constant.name, values: args });
|
|
569
|
+
else if (constant.name === "subtract" && args.length >= 2)
|
|
570
|
+
calculations.push({
|
|
571
|
+
target,
|
|
572
|
+
op: "subtract",
|
|
573
|
+
base: args[0],
|
|
574
|
+
subtract: args.slice(1),
|
|
575
|
+
});
|
|
576
|
+
else if (constant.name === "multiply" && args.length === 2)
|
|
577
|
+
calculations.push({
|
|
578
|
+
target,
|
|
579
|
+
op: "multiply",
|
|
580
|
+
amount: args[0],
|
|
581
|
+
units: args[1],
|
|
582
|
+
});
|
|
583
|
+
else if (constant.name === "divide" && args.length === 2)
|
|
584
|
+
calculations.push({
|
|
585
|
+
target,
|
|
586
|
+
op: "divide",
|
|
587
|
+
amount: args[0],
|
|
588
|
+
divisor: args[1],
|
|
589
|
+
rounding: "floor",
|
|
590
|
+
});
|
|
591
|
+
else if (["before", "after"].includes(constant.name) &&
|
|
592
|
+
args.length === 2)
|
|
593
|
+
calculations.push({
|
|
594
|
+
target,
|
|
595
|
+
op: "shift",
|
|
596
|
+
date: args[0],
|
|
597
|
+
milliseconds: args[1],
|
|
598
|
+
direction: constant.name,
|
|
599
|
+
});
|
|
600
|
+
else
|
|
601
|
+
fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
|
|
602
|
+
}
|
|
603
|
+
else if (type === "enum" && constant.kind === "name")
|
|
604
|
+
f.value = constant.value;
|
|
605
|
+
else if (type === "money" && constant.kind === "name")
|
|
606
|
+
calculations.push({
|
|
607
|
+
target: row.key,
|
|
608
|
+
op: "sum",
|
|
609
|
+
values: [val(constant)],
|
|
610
|
+
});
|
|
611
|
+
else
|
|
612
|
+
f.value = literal(constant);
|
|
613
|
+
}
|
|
614
|
+
result.push(f);
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
};
|
|
618
|
+
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
619
|
+
const lifecycle = data(body.get("lifecycle") ?? emptyBlock);
|
|
620
|
+
lifecycle.transitions = {};
|
|
621
|
+
const inst = {
|
|
622
|
+
id,
|
|
623
|
+
title: title(id),
|
|
624
|
+
summary: body.has("summary")
|
|
625
|
+
? String(data(body.get("summary")))
|
|
626
|
+
: title(id),
|
|
627
|
+
fields,
|
|
628
|
+
calculate: calculations,
|
|
629
|
+
lifecycle,
|
|
630
|
+
actions: {},
|
|
631
|
+
actionOrder: [],
|
|
632
|
+
};
|
|
633
|
+
for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
|
|
634
|
+
const name = row.key.slice(7);
|
|
635
|
+
const selected = (block) => ({
|
|
636
|
+
...block,
|
|
637
|
+
entries: block.entries.flatMap((entry) => {
|
|
638
|
+
if (!entry.key.startsWith("when "))
|
|
639
|
+
return [entry];
|
|
640
|
+
const [, tunable, , choice] = entry.key.split(" ");
|
|
641
|
+
const binding = environment.get(tunable);
|
|
642
|
+
if (!binding)
|
|
643
|
+
fail(entry, `unknown branch tunable ${tunable}`, "name an enum tunable declared by this header");
|
|
644
|
+
if (!enums.get(tunable)?.includes(choice))
|
|
645
|
+
fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
|
|
646
|
+
const body = asBlock(entry.value);
|
|
647
|
+
for (const clause of body.entries)
|
|
648
|
+
if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
|
|
649
|
+
!clause.key.startsWith("when "))
|
|
650
|
+
fail(clause, "when permits requirements, calculations, moves and invocations", "keep lifecycle and actor clauses outside the branch");
|
|
651
|
+
return text(binding) === choice ? selected(body).entries : [];
|
|
652
|
+
}),
|
|
653
|
+
});
|
|
654
|
+
const slots = entries(selected(asBlock(row.value)));
|
|
655
|
+
const a = {
|
|
656
|
+
summary: slots.has("summary")
|
|
657
|
+
? String(data(slots.get("summary")))
|
|
658
|
+
: `${title(name)} ${id}`,
|
|
659
|
+
publicAction: camel(`${name}_${id}`),
|
|
660
|
+
event: `${id}.${name}`,
|
|
661
|
+
actor: "caller",
|
|
662
|
+
input: lowerFields(asBlock(slots.get("input"))),
|
|
663
|
+
requires: [],
|
|
664
|
+
moves: [],
|
|
665
|
+
};
|
|
666
|
+
if (name !== "create") {
|
|
667
|
+
const from = slots.get("from");
|
|
668
|
+
const to = slots.get("to");
|
|
669
|
+
if (!from || !to)
|
|
670
|
+
fail(row, `action ${name} needs from and to`, "declare its source and destination states");
|
|
671
|
+
lifecycle.transitions[name] = {
|
|
672
|
+
from: (from.kind === "list" ? from.items : [from]).map((e) => String(data(e))),
|
|
673
|
+
to: String(data(to)),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
for (const [key, expr] of slots) {
|
|
677
|
+
if (["from", "to", "input", "summary"].includes(key))
|
|
678
|
+
continue;
|
|
679
|
+
if (key === "moves") {
|
|
680
|
+
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
681
|
+
for (const [index, move] of moves.entries()) {
|
|
682
|
+
const parts = entries(asBlock(move));
|
|
683
|
+
const op = parts.has("operation")
|
|
684
|
+
? String(data(parts.get("operation")))
|
|
685
|
+
: "internal_transfer.create";
|
|
686
|
+
if (op === "internal_transfer.reserve" && !parts.has("capture"))
|
|
687
|
+
fail(move, "reserve needs a receipt field", "add capture: receipt_name");
|
|
688
|
+
if ((op === "internal_transfer.post" ||
|
|
689
|
+
op === "internal_transfer.void") &&
|
|
690
|
+
!parts.has("transfer"))
|
|
691
|
+
fail(move, "post and void need a receipt", "add transfer: receipt_name");
|
|
692
|
+
const key = parts.has("key")
|
|
693
|
+
? String(data(parts.get("key")))
|
|
694
|
+
: `move${index + 1}`;
|
|
695
|
+
if (op === "internal_transfer.create" ||
|
|
696
|
+
op === "internal_transfer.reserve") {
|
|
697
|
+
const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
|
|
698
|
+
if (parts.has("shares")) {
|
|
699
|
+
if (!amount ||
|
|
700
|
+
!from ||
|
|
701
|
+
op !== "internal_transfer.create" ||
|
|
702
|
+
parts.has("fee"))
|
|
703
|
+
fail(move, "split needs a posted amount and source", "declare amount, from and shares without a fee");
|
|
704
|
+
const shares = [
|
|
705
|
+
...entries(asBlock(resolve(parts.get("shares")))),
|
|
706
|
+
];
|
|
707
|
+
if (!shares.length || shares.length > 64)
|
|
708
|
+
fail(move, "split needs 1 to 64 recipients", "name the receiving parties and percentages");
|
|
709
|
+
let total = 0;
|
|
710
|
+
const pieces = [];
|
|
711
|
+
for (const [index, [party, rate]] of shares.entries()) {
|
|
712
|
+
const recipient = resolve({
|
|
713
|
+
kind: "name",
|
|
714
|
+
value: party,
|
|
715
|
+
span: rate.span,
|
|
716
|
+
});
|
|
717
|
+
if (recipient.kind !== "name" ||
|
|
718
|
+
!document.parties[recipient.value] ||
|
|
719
|
+
rate.kind !== "percent")
|
|
720
|
+
fail(rate, "split needs party percentages", "write party_name: 70%");
|
|
721
|
+
total += Number(literal(rate));
|
|
722
|
+
const target = `${name}_${key}_share${index + 1}`;
|
|
723
|
+
if (fields.some((f) => f.name === target))
|
|
724
|
+
fail(move, "split field name conflicts", "rename the move key");
|
|
725
|
+
fields.push({ name: target, type: "money" });
|
|
726
|
+
calculations.push(index === shares.length - 1 && pieces.length
|
|
727
|
+
? {
|
|
728
|
+
target,
|
|
729
|
+
op: "subtract",
|
|
730
|
+
base: val(amount),
|
|
731
|
+
subtract: [...pieces],
|
|
732
|
+
}
|
|
733
|
+
: {
|
|
734
|
+
target,
|
|
735
|
+
op: "rate",
|
|
736
|
+
base: val(amount),
|
|
737
|
+
bps: { literal: literal(rate) },
|
|
738
|
+
rounding: "floor",
|
|
739
|
+
});
|
|
740
|
+
const piece = { field: `self.${target}` };
|
|
741
|
+
pieces.push(piece);
|
|
742
|
+
a.moves.push({
|
|
743
|
+
key: `${key}_${index + 1}`,
|
|
744
|
+
operation: "internal_transfer.create",
|
|
745
|
+
amount: piece,
|
|
746
|
+
from: path(from),
|
|
747
|
+
to: `party.${recipient.value}`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (total !== 10000)
|
|
751
|
+
fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (!amount || !from || !to)
|
|
755
|
+
fail(move, "move needs amount, from and to", "declare all three move operands");
|
|
756
|
+
const transfer = {
|
|
757
|
+
key,
|
|
758
|
+
operation: op,
|
|
759
|
+
...(parts.has("capture")
|
|
760
|
+
? { capture: String(data(parts.get("capture"))) }
|
|
761
|
+
: {}),
|
|
762
|
+
amount: val(amount),
|
|
763
|
+
from: path(from),
|
|
764
|
+
to: path(to),
|
|
765
|
+
};
|
|
766
|
+
const fee = parts.get("fee");
|
|
767
|
+
if (!fee)
|
|
768
|
+
a.moves.push(op === "internal_transfer.reserve"
|
|
769
|
+
? {
|
|
770
|
+
...transfer,
|
|
771
|
+
operation: op,
|
|
772
|
+
capture: String(data(parts.get("capture"))),
|
|
773
|
+
}
|
|
774
|
+
: { ...transfer, operation: op });
|
|
775
|
+
else {
|
|
776
|
+
if (op !== "internal_transfer.create")
|
|
777
|
+
fail(move, "fees settle with a posted transfer", "declare a separate posted settlement after releasing the reservation");
|
|
778
|
+
const terms = entries(asBlock(resolve(fee)));
|
|
779
|
+
for (const term of terms.keys())
|
|
780
|
+
if (!["seller", "buyer", "tax"].includes(term))
|
|
781
|
+
fail(fee, `unknown fee term ${term}`, "choose seller or buyer, and optional tax");
|
|
782
|
+
const paidBy = terms.has("seller") ? "seller" : "buyer";
|
|
783
|
+
if (terms.has("seller") === terms.has("buyer"))
|
|
784
|
+
fail(fee, "fee needs exactly one paying party", "write seller: 1% or buyer: 1%");
|
|
785
|
+
const quoted = terms.get(paidBy);
|
|
786
|
+
const rate = quoted.kind === "capped" ? quoted.rate : quoted;
|
|
787
|
+
if (rate.kind !== "percent")
|
|
788
|
+
fail(rate, "fee rate needs a percentage", "write a percentage such as 1%");
|
|
789
|
+
const prefix = `${name}_${key}`;
|
|
790
|
+
const derived = (suffix, calculation) => {
|
|
791
|
+
const target = prefix + "_" + suffix;
|
|
792
|
+
if (fields.some((f) => f.name === target))
|
|
793
|
+
fail(move, `generated fee field ${target} conflicts with a field`, "rename the authored field or move key");
|
|
794
|
+
fields.push({ name: target, type: "money" });
|
|
795
|
+
calculations.push({
|
|
796
|
+
...calculation,
|
|
797
|
+
target,
|
|
798
|
+
});
|
|
799
|
+
return { field: `self.${target}` };
|
|
800
|
+
};
|
|
801
|
+
const gross = derived("feeGross", {
|
|
802
|
+
op: "rate",
|
|
803
|
+
base: transfer.amount,
|
|
804
|
+
bps: { literal: literal(rate) },
|
|
805
|
+
rounding: "floor",
|
|
806
|
+
});
|
|
807
|
+
let charge = gross;
|
|
808
|
+
if (quoted.kind === "capped") {
|
|
809
|
+
if (quoted.cap.kind !== "money")
|
|
810
|
+
fail(quoted.cap, "fee cap needs money", "write an amount such as 750 SAR");
|
|
811
|
+
charge = derived("fee", {
|
|
812
|
+
op: "minimum",
|
|
813
|
+
values: [gross, { literal: literal(quoted.cap) }],
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const tax = terms.get("tax");
|
|
817
|
+
if (tax && tax.kind !== "percent")
|
|
818
|
+
fail(tax, "tax needs a percentage of the fee", "write tax: 15%");
|
|
819
|
+
const vat = tax
|
|
820
|
+
? derived("tax", {
|
|
821
|
+
op: "rate",
|
|
822
|
+
base: charge,
|
|
823
|
+
bps: { literal: literal(tax) },
|
|
824
|
+
rounding: "floor",
|
|
825
|
+
})
|
|
826
|
+
: undefined;
|
|
827
|
+
const charges = vat ? [charge, vat] : [charge];
|
|
828
|
+
const net = paidBy === "seller"
|
|
829
|
+
? derived("net", {
|
|
830
|
+
op: "subtract",
|
|
831
|
+
base: transfer.amount,
|
|
832
|
+
subtract: charges,
|
|
833
|
+
})
|
|
834
|
+
: transfer.amount;
|
|
835
|
+
for (const [party, role] of [
|
|
836
|
+
["programOperator", "program_operator"],
|
|
837
|
+
["programTax", "tax_payable"],
|
|
838
|
+
]) {
|
|
839
|
+
if (document.parties[party] &&
|
|
840
|
+
document.parties[party].role !== role)
|
|
841
|
+
fail(move, `${party} is reserved for fees`, "rename this party");
|
|
842
|
+
document.parties[party] = { kind: "business", role };
|
|
843
|
+
}
|
|
844
|
+
a.moves.push({
|
|
845
|
+
...transfer,
|
|
846
|
+
operation: "internal_transfer.create",
|
|
847
|
+
amount: net,
|
|
848
|
+
});
|
|
849
|
+
a.moves.push({
|
|
850
|
+
key: key + "Fee",
|
|
851
|
+
operation: "internal_transfer.create",
|
|
852
|
+
amount: charge,
|
|
853
|
+
from: transfer.from,
|
|
854
|
+
to: "party.programOperator",
|
|
855
|
+
});
|
|
856
|
+
if (vat)
|
|
857
|
+
a.moves.push({
|
|
858
|
+
key: key + "Tax",
|
|
859
|
+
operation: "internal_transfer.create",
|
|
860
|
+
amount: vat,
|
|
861
|
+
from: transfer.from,
|
|
862
|
+
to: "party.programTax",
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else if (op === "internal_transfer.post" ||
|
|
867
|
+
op === "internal_transfer.void")
|
|
868
|
+
a.moves.push({
|
|
869
|
+
key,
|
|
870
|
+
operation: op,
|
|
871
|
+
transfer: path(parts.get("transfer")),
|
|
872
|
+
...(parts.has("capture")
|
|
873
|
+
? { capture: String(data(parts.get("capture"))) }
|
|
874
|
+
: {}),
|
|
875
|
+
});
|
|
876
|
+
else
|
|
877
|
+
fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else if (key === "approval") {
|
|
881
|
+
const approval = data(expr);
|
|
882
|
+
if (approval && approval.input === "material") {
|
|
883
|
+
approval.input = {};
|
|
884
|
+
materialApprovals.add(a);
|
|
885
|
+
}
|
|
886
|
+
a.approval = approval;
|
|
887
|
+
}
|
|
888
|
+
else
|
|
889
|
+
a[key] = data(expr);
|
|
890
|
+
}
|
|
891
|
+
if (automatic(a.actor))
|
|
892
|
+
delete a.publicAction;
|
|
893
|
+
for (const requirement of a.requires ?? []) {
|
|
894
|
+
if (requirement.kind !== "approval" ||
|
|
895
|
+
!approvers.has(requirement.party))
|
|
896
|
+
continue;
|
|
897
|
+
const action = requirement.action ?? name;
|
|
898
|
+
const key = `${id}_${action}_decision`;
|
|
899
|
+
const previous = implicitDecisions.get(key);
|
|
900
|
+
if (previous && previous.party !== requirement.party)
|
|
901
|
+
fail({ span: origin }, `action ${action} has multiple approval parties`, "use a separate decision action for each party");
|
|
902
|
+
implicitDecisions.set(key, {
|
|
903
|
+
target: id,
|
|
904
|
+
action,
|
|
905
|
+
party: requirement.party,
|
|
906
|
+
origin,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
inst.actions[name] = a;
|
|
910
|
+
inst.actionOrder.push(name);
|
|
911
|
+
}
|
|
912
|
+
for (const key of ["invariants"])
|
|
913
|
+
if (body.has(key))
|
|
914
|
+
inst[key] = data(body.get(key));
|
|
915
|
+
origins.push({
|
|
916
|
+
path: `$.instruments[${document.instruments.length}]`,
|
|
917
|
+
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
918
|
+
});
|
|
919
|
+
document.instruments.push(inst);
|
|
920
|
+
for (const [key, definition] of records) {
|
|
921
|
+
const child = {
|
|
922
|
+
kind: "instrument",
|
|
923
|
+
name: key,
|
|
924
|
+
parameters: [],
|
|
925
|
+
body: asBlock(definition),
|
|
926
|
+
span: origin,
|
|
927
|
+
};
|
|
928
|
+
addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
|
|
929
|
+
...environment,
|
|
930
|
+
["parent", { kind: "name", value: id, span: origin }],
|
|
931
|
+
]), approvers, enums);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
for (const decl of program.decls) {
|
|
935
|
+
if (decl.kind === "instrument") {
|
|
936
|
+
if (decl.parameters.length)
|
|
937
|
+
fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
|
|
938
|
+
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
939
|
+
}
|
|
940
|
+
if (decl.kind === "object") {
|
|
941
|
+
const template = templates.get(decl.object);
|
|
942
|
+
if (!template)
|
|
943
|
+
fail(decl, `unknown object ${decl.object}`, `add use ${decl.object.split(".")[0]} and choose a declared object`);
|
|
944
|
+
addInstrument(template, decl.name, decl.body, decl.span);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const eliminatedStates = new Map();
|
|
948
|
+
// Remove branches excluded by immutable tunables before checking reference states.
|
|
949
|
+
for (const inst of document.instruments) {
|
|
950
|
+
for (const [key, action] of Object.entries(inst.actions)) {
|
|
951
|
+
const constant = (value) => {
|
|
952
|
+
if ("literal" in value)
|
|
953
|
+
return value.literal;
|
|
954
|
+
const f = inst.fields.find((f) => `self.${f.name}` === value.field);
|
|
955
|
+
return f && "value" in f ? f.value : undefined;
|
|
956
|
+
};
|
|
957
|
+
if (action.requires.some((r) => {
|
|
958
|
+
if (r.kind !== "compare")
|
|
959
|
+
return false;
|
|
960
|
+
const left = constant(r.left), right = constant(r.right);
|
|
961
|
+
if (left === undefined || right === undefined)
|
|
962
|
+
return false;
|
|
963
|
+
return r.operator === "=="
|
|
964
|
+
? left !== right
|
|
965
|
+
: r.operator === "!="
|
|
966
|
+
? left === right
|
|
967
|
+
: false;
|
|
968
|
+
})) {
|
|
969
|
+
delete inst.actions[key];
|
|
970
|
+
delete inst.lifecycle.transitions[key];
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const reachable = new Set([inst.lifecycle.initial]);
|
|
974
|
+
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
975
|
+
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
976
|
+
if (edge.from.some((state) => reachable.has(state)))
|
|
977
|
+
reachable.add(edge.to);
|
|
978
|
+
for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
|
|
979
|
+
edge.from = edge.from.filter((state) => reachable.has(state));
|
|
980
|
+
if (!edge.from.length) {
|
|
981
|
+
delete inst.actions[key];
|
|
982
|
+
delete inst.lifecycle.transitions[key];
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
eliminatedStates.set(inst.id, new Set(inst.lifecycle.states.filter((state) => !reachable.has(state))));
|
|
986
|
+
inst.lifecycle.states = inst.lifecycle.states.filter((state) => reachable.has(state));
|
|
987
|
+
inst.actionOrder = inst.actionOrder.filter((key) => inst.actions[key]);
|
|
988
|
+
}
|
|
989
|
+
for (const inst of document.instruments)
|
|
990
|
+
for (const action of Object.values(inst.actions)) {
|
|
991
|
+
for (const requirement of action.requires) {
|
|
992
|
+
if (requirement.kind !== "state")
|
|
993
|
+
continue;
|
|
994
|
+
const field = inst.fields.find((f) => `self.${f.name}` === requirement.reference);
|
|
995
|
+
if (field?.type === "ref")
|
|
996
|
+
requirement.states = requirement.states.filter((state) => !(typeof field.target === "string"
|
|
997
|
+
? [field.target]
|
|
998
|
+
: field.target).some((id) => eliminatedStates.get(id)?.has(state)));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Approval tunables instantiate the same library record as an explicit decision.
|
|
1002
|
+
if (implicitDecisions.size) {
|
|
1003
|
+
const content = (options.standardLibrary ?? bundledStandardLibrary).source("approvals");
|
|
1004
|
+
const parsed = content ? parseProgram(content) : undefined;
|
|
1005
|
+
const template = parsed?.program.decls.find((decl) => decl.kind === "instrument" && decl.name === "decision");
|
|
1006
|
+
if (!template ||
|
|
1007
|
+
template.kind !== "instrument" ||
|
|
1008
|
+
parsed?.diagnostics.length)
|
|
1009
|
+
fail(program, "implicit decisions need the approvals header", "provide the standard approvals header");
|
|
1010
|
+
for (const [id, decision] of implicitDecisions) {
|
|
1011
|
+
if (!document.instruments.find((inst) => inst.id === decision.target)
|
|
1012
|
+
?.actions[decision.action])
|
|
1013
|
+
continue;
|
|
1014
|
+
const existing = document.instruments.some((inst) => Object.values(inst.actions).some((action) => action.approval?.action === decision.action &&
|
|
1015
|
+
action.approval.party === decision.party &&
|
|
1016
|
+
inst.fields.some((field) => field.type === "ref" &&
|
|
1017
|
+
field.target === decision.target &&
|
|
1018
|
+
`self.${field.name}` === action.approval?.target)));
|
|
1019
|
+
if (existing)
|
|
1020
|
+
continue;
|
|
1021
|
+
if (document.instruments.some((inst) => inst.id === id))
|
|
1022
|
+
fail({ span: decision.origin }, `implicit decision name ${id} is already used`, "rename the conflicting object");
|
|
1023
|
+
addInstrument(template, id, {
|
|
1024
|
+
kind: "block",
|
|
1025
|
+
span: decision.origin,
|
|
1026
|
+
entries: Object.entries({
|
|
1027
|
+
for: decision.target,
|
|
1028
|
+
approved_by: decision.party,
|
|
1029
|
+
action: decision.action,
|
|
1030
|
+
}).map(([key, value]) => ({
|
|
1031
|
+
key,
|
|
1032
|
+
value: {
|
|
1033
|
+
kind: key === "action" ? "text" : "name",
|
|
1034
|
+
value,
|
|
1035
|
+
span: decision.origin,
|
|
1036
|
+
},
|
|
1037
|
+
span: decision.origin,
|
|
1038
|
+
})),
|
|
1039
|
+
}, decision.origin);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Material approval fields are inferred from the selected action's typed input.
|
|
1043
|
+
for (const inst of document.instruments)
|
|
1044
|
+
for (const action of Object.values(inst.actions)) {
|
|
1045
|
+
if (!materialApprovals.has(action) || !action.approval)
|
|
1046
|
+
continue;
|
|
1047
|
+
const targetField = inst.fields.find((f) => `self.${f.name}` === action.approval.target);
|
|
1048
|
+
if (targetField?.type !== "ref")
|
|
1049
|
+
continue;
|
|
1050
|
+
const target = document.instruments.find((i) => i.id === targetField.target)?.actions[action.approval.action];
|
|
1051
|
+
if (!target)
|
|
1052
|
+
continue;
|
|
1053
|
+
for (const field of target.input) {
|
|
1054
|
+
const name = `material_${field.name}`;
|
|
1055
|
+
if (!inst.fields.some((f) => f.name === name))
|
|
1056
|
+
inst.fields.push({ ...field, name });
|
|
1057
|
+
action.approval.input[field.name] = { field: `self.${name}` };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const changed = new Set();
|
|
1061
|
+
for (const decl of program.decls)
|
|
1062
|
+
if (decl.kind === "hide" || decl.kind === "expose") {
|
|
1063
|
+
const parts = decl.target.split(".");
|
|
1064
|
+
const key = parts.pop();
|
|
1065
|
+
const id = parts.join("_");
|
|
1066
|
+
const action = document.instruments.find((i) => i.id === id)?.actions[key ?? ""];
|
|
1067
|
+
if (!action || changed.has(decl.target))
|
|
1068
|
+
fail(decl, `unknown or repeated action ${decl.target}`, "name one declared object.action once");
|
|
1069
|
+
changed.add(decl.target);
|
|
1070
|
+
if (decl.kind === "hide")
|
|
1071
|
+
delete action.publicAction;
|
|
1072
|
+
else if (automatic(action.actor))
|
|
1073
|
+
fail(decl, `${decl.target} runs on the clock or its parent, not a caller`, "expose only caller actions");
|
|
1074
|
+
else
|
|
1075
|
+
action.publicAction = decl.name;
|
|
1076
|
+
}
|
|
1077
|
+
const usedParties = new Set();
|
|
1078
|
+
const collectParties = (value) => {
|
|
1079
|
+
if (typeof value === "string") {
|
|
1080
|
+
usedParties.add(value);
|
|
1081
|
+
if (value.startsWith("party."))
|
|
1082
|
+
usedParties.add(value.split(".")[1]);
|
|
1083
|
+
}
|
|
1084
|
+
else if (Array.isArray(value))
|
|
1085
|
+
value.forEach(collectParties);
|
|
1086
|
+
else if (value && typeof value === "object")
|
|
1087
|
+
Object.values(value).forEach(collectParties);
|
|
1088
|
+
};
|
|
1089
|
+
collectParties(document.instruments);
|
|
1090
|
+
for (const party of ["programFines", "programCosts"])
|
|
1091
|
+
if (!usedParties.has(party) && !names.has(party))
|
|
1092
|
+
delete document.parties[party];
|
|
1093
|
+
const validated = validateUdl(document);
|
|
1094
|
+
if (!validated.ok)
|
|
1095
|
+
return {
|
|
1096
|
+
verdict: "invalid",
|
|
1097
|
+
diagnostics: validated.issues.map((i) => {
|
|
1098
|
+
const origin = [...origins]
|
|
1099
|
+
.reverse()
|
|
1100
|
+
.find((o) => i.path.startsWith(o.path));
|
|
1101
|
+
return diagnostic({
|
|
1102
|
+
code: "HSX1601",
|
|
1103
|
+
message: `${i.path}: ${i.message}`,
|
|
1104
|
+
fix: i.fix,
|
|
1105
|
+
span: origin?.span ?? program.span,
|
|
1106
|
+
}, "lower");
|
|
1107
|
+
}),
|
|
1108
|
+
};
|
|
1109
|
+
return {
|
|
1110
|
+
verdict: "valid",
|
|
1111
|
+
diagnostics: [],
|
|
1112
|
+
artifacts: {
|
|
1113
|
+
document: validated.value,
|
|
1114
|
+
costManifest: buildUdlCostManifest(validated.value),
|
|
1115
|
+
originMap: origins,
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
if (error instanceof CompileFailure)
|
|
1121
|
+
return {
|
|
1122
|
+
verdict: "invalid",
|
|
1123
|
+
diagnostics: [diagnostic(error.diagnostic, "check")],
|
|
1124
|
+
};
|
|
1125
|
+
throw error;
|
|
159
1126
|
}
|
|
160
|
-
offsets[source.length] = bytes;
|
|
161
|
-
return offsets;
|
|
162
1127
|
}
|
|
163
1128
|
//# sourceMappingURL=compile.js.map
|