@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/src/compile.ts
CHANGED
|
@@ -1,273 +1,1561 @@
|
|
|
1
|
-
|
|
2
|
-
* The HSX compiler driver: source text in, three-verdict result out.
|
|
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
|
-
|
|
14
|
-
import type { UdlIssueCode } from "@hyperscale0/udl";
|
|
15
|
-
import type { ResolvedProgramActionPlan } from "./ir.ts";
|
|
16
|
-
import { lineColIn, lineIndex } from "./ast.ts";
|
|
17
|
-
import {
|
|
18
|
-
buildCostManifest,
|
|
19
|
-
type UdlCostManifest,
|
|
20
|
-
type UdlCostTables,
|
|
21
|
-
} from "./cost.ts";
|
|
22
|
-
import { lowerGeneralProgram, type OriginMapEntry } from "./emit.ts";
|
|
1
|
+
import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
|
|
23
2
|
import {
|
|
24
|
-
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
|
|
3
|
+
validateUdl,
|
|
4
|
+
type UdlAction,
|
|
5
|
+
type UdlCalculation,
|
|
6
|
+
type UdlDocument,
|
|
7
|
+
type UdlField,
|
|
8
|
+
type UdlInstrument,
|
|
9
|
+
type UdlValue,
|
|
10
|
+
} from "@hyperscale0/udl";
|
|
11
|
+
import { tunableBounds } from "./tunables.ts";
|
|
28
12
|
import { parseProgram } from "./parse.ts";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
type
|
|
13
|
+
import {
|
|
14
|
+
lineColAt,
|
|
15
|
+
type BlockExpr,
|
|
16
|
+
type Diagnostic,
|
|
17
|
+
type Expr,
|
|
18
|
+
type InstrumentDecl,
|
|
19
|
+
type ObjectDecl,
|
|
20
|
+
type Span,
|
|
21
|
+
} from "./ast.ts";
|
|
22
|
+
import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
|
|
34
23
|
|
|
35
|
-
export interface CompileDiagnostic {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** 1-indexed source line. */
|
|
41
|
-
readonly line: number;
|
|
42
|
-
readonly message: string;
|
|
43
|
-
/** Source offsets in UTF-16 code units, matching parser and checker spans. */
|
|
44
|
-
readonly span: { readonly end: number; readonly start: number };
|
|
45
|
-
/** UTF-8 byte span in root source, omitted when referring to an imported module. */
|
|
46
|
-
readonly byteSpan?: { readonly end: number; readonly start: number };
|
|
47
|
-
readonly code?: string;
|
|
48
|
-
readonly fix?: string;
|
|
49
|
-
/** Canonical UDL path when lowering rejected an emitted clause. */
|
|
50
|
-
readonly path?: string;
|
|
51
|
-
/** UDL validator category when the emitter rejected its candidate document. */
|
|
52
|
-
readonly udlCode?: UdlIssueCode;
|
|
53
|
-
readonly severity: "error" | "warning";
|
|
54
|
-
/** The stage that raised it: parse, check, or lower. */
|
|
55
|
-
readonly stage: "bind" | "check" | "lower" | "parse" | "typecheck";
|
|
24
|
+
export interface CompileDiagnostic extends Diagnostic {
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
27
|
+
severity: "error";
|
|
28
|
+
stage: "parse" | "check" | "lower";
|
|
56
29
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** The canonical UDL document. */
|
|
61
|
-
readonly document: Json;
|
|
62
|
-
/** Canonical UDL paths bound to their narrowest authored source terms. */
|
|
63
|
-
readonly originMap: readonly CompileOriginMapEntry[];
|
|
64
|
-
/** Deterministic compile-time cost, pinned to one versioned table. */
|
|
65
|
-
readonly costManifest: UdlCostManifest;
|
|
30
|
+
export interface CompileOriginMapEntry {
|
|
31
|
+
path: string;
|
|
32
|
+
span: Span & { line: number; column: number };
|
|
66
33
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/** One table, or one per billing currency; the program's money fields pick. */
|
|
70
|
-
readonly costTable?: UdlCostTables;
|
|
34
|
+
export interface CompileOptions {
|
|
35
|
+
standardLibrary?: StandardLibrary;
|
|
71
36
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
readonly column: number;
|
|
80
|
-
/** 1-indexed source line. */
|
|
81
|
-
readonly line: number;
|
|
82
|
-
/** Inclusive UTF-8 byte offset. */
|
|
83
|
-
readonly start: number;
|
|
37
|
+
export interface CompileResult {
|
|
38
|
+
verdict: "valid" | "invalid";
|
|
39
|
+
diagnostics: CompileDiagnostic[];
|
|
40
|
+
artifacts?: {
|
|
41
|
+
document: UdlDocument;
|
|
42
|
+
costManifest: UdlCostManifest;
|
|
43
|
+
originMap: CompileOriginMapEntry[];
|
|
84
44
|
};
|
|
85
45
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
46
|
+
class CompileFailure extends Error {
|
|
47
|
+
constructor(readonly diagnostic: Diagnostic) {
|
|
48
|
+
super(diagnostic.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function fail(expr: { span: Span }, message: string, fix: string): never {
|
|
52
|
+
throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
|
|
53
|
+
}
|
|
54
|
+
const emptyBlock: BlockExpr = {
|
|
55
|
+
kind: "block",
|
|
56
|
+
entries: [],
|
|
57
|
+
span: { start: 0, end: 0 },
|
|
58
|
+
};
|
|
59
|
+
const camel = (name: string) =>
|
|
60
|
+
name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
61
|
+
/** Clock and parent actors run without a caller, so they have no public name. */
|
|
62
|
+
const automatic = (actor: UdlAction["actor"]) =>
|
|
63
|
+
actor === "clock" || (typeof actor === "object" && "parent" in actor);
|
|
64
|
+
const title = (name: string) =>
|
|
65
|
+
name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
|
|
66
|
+
function entries(block: BlockExpr): Map<string, Expr> {
|
|
67
|
+
const map = new Map<string, Expr>();
|
|
68
|
+
for (const row of block.entries) {
|
|
69
|
+
const previous = map.get(row.key);
|
|
70
|
+
if (previous) {
|
|
71
|
+
if (
|
|
72
|
+
!["requires", "moves", "invariants", "invoke", "calculate"].includes(
|
|
73
|
+
row.key,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
fail(row, `duplicate ${row.key}`, "keep one value for this name");
|
|
77
|
+
const items = (expr: Expr) =>
|
|
78
|
+
expr.kind === "list" ? expr.items : [expr];
|
|
79
|
+
map.set(row.key, {
|
|
80
|
+
kind: "list",
|
|
81
|
+
items: [...items(previous), ...items(row.value)],
|
|
82
|
+
span: row.span,
|
|
83
|
+
});
|
|
84
|
+
} else map.set(row.key, row.value);
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
function asBlock(expr: Expr | undefined): BlockExpr {
|
|
89
|
+
if (!expr) return emptyBlock;
|
|
90
|
+
if (expr.kind !== "block")
|
|
91
|
+
fail(expr, "expected a block", "write { name: value }");
|
|
92
|
+
return expr;
|
|
93
|
+
}
|
|
94
|
+
function text(expr: Expr): string {
|
|
95
|
+
if ("value" in expr && typeof expr.value === "string") return expr.value;
|
|
96
|
+
return fail(
|
|
97
|
+
expr,
|
|
98
|
+
"expected a name or literal",
|
|
99
|
+
"write a single name or literal",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function decimal(raw: string, scale: number, expr: Expr): string {
|
|
103
|
+
const [whole = "", fraction = ""] = raw.split(".");
|
|
104
|
+
if (!/^\d+$/.test(whole) || fraction.length > scale)
|
|
105
|
+
fail(
|
|
106
|
+
expr,
|
|
107
|
+
`literal has more than ${scale} decimal places`,
|
|
108
|
+
`write at most ${scale} decimal places`,
|
|
109
|
+
);
|
|
110
|
+
return (
|
|
111
|
+
BigInt(whole) * 10n ** BigInt(scale) +
|
|
112
|
+
BigInt(fraction.padEnd(scale, "0") || "0")
|
|
113
|
+
).toString();
|
|
114
|
+
}
|
|
115
|
+
function literal(expr: Expr): string | number | boolean {
|
|
116
|
+
if (expr.kind === "money") {
|
|
117
|
+
const [raw, currency] = expr.value.split(" ");
|
|
118
|
+
if (currency !== "SAR")
|
|
119
|
+
fail(
|
|
120
|
+
expr,
|
|
121
|
+
`currency ${currency} differs from SAR`,
|
|
122
|
+
"write this amount in SAR",
|
|
123
|
+
);
|
|
124
|
+
const result = decimal(raw!, 2, expr);
|
|
125
|
+
if (result.length > 18)
|
|
126
|
+
fail(
|
|
127
|
+
expr,
|
|
128
|
+
"amount exceeds 18 minor-unit digits",
|
|
129
|
+
"write a smaller amount",
|
|
130
|
+
);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
if (expr.kind === "percent") {
|
|
134
|
+
const n = Number(decimal(expr.value, 2, expr));
|
|
135
|
+
if (n > 10000)
|
|
136
|
+
fail(
|
|
137
|
+
expr,
|
|
138
|
+
"percentage exceeds 100%",
|
|
139
|
+
"write a percentage from 0% to 100%",
|
|
140
|
+
);
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
if (expr.kind === "number") {
|
|
144
|
+
const n = Number(expr.value);
|
|
145
|
+
if (!Number.isSafeInteger(n))
|
|
146
|
+
fail(
|
|
147
|
+
expr,
|
|
148
|
+
"expected a safe integer",
|
|
149
|
+
"write an integer, a percentage, or an amount with SAR",
|
|
150
|
+
);
|
|
151
|
+
return n;
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
expr.kind === "duration" ||
|
|
155
|
+
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)) ||
|
|
156
|
+
(expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))
|
|
157
|
+
) {
|
|
158
|
+
const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
|
|
159
|
+
const units: Record<string, number> = {
|
|
160
|
+
ms: 1,
|
|
161
|
+
s: 1000,
|
|
162
|
+
m: 60000,
|
|
163
|
+
h: 3600000,
|
|
164
|
+
d: 86400000,
|
|
165
|
+
w: 604800000,
|
|
166
|
+
};
|
|
167
|
+
const iso =
|
|
168
|
+
/^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
|
|
169
|
+
expr.value,
|
|
170
|
+
);
|
|
171
|
+
const n = short
|
|
172
|
+
? Number(short[1]) * units[short[2]!]!
|
|
173
|
+
: iso
|
|
174
|
+
? Number(iso[1] ?? 0) * units.w! +
|
|
175
|
+
Number(iso[2] ?? 0) * units.d! +
|
|
176
|
+
Number(iso[3] ?? 0) * units.h! +
|
|
177
|
+
Number(iso[4] ?? 0) * units.m! +
|
|
178
|
+
Number(iso[5] ?? 0) * units.s!
|
|
179
|
+
: NaN;
|
|
180
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
181
|
+
fail(
|
|
182
|
+
expr,
|
|
183
|
+
"invalid duration",
|
|
184
|
+
"write a positive fixed duration such as 48h or 3d",
|
|
185
|
+
);
|
|
186
|
+
return n;
|
|
187
|
+
}
|
|
188
|
+
if (expr.kind === "date") {
|
|
189
|
+
const raw =
|
|
190
|
+
expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
|
|
191
|
+
const n = Date.parse(raw);
|
|
192
|
+
if (
|
|
193
|
+
!Number.isFinite(n) ||
|
|
194
|
+
new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
|
|
195
|
+
raw.slice(0, 10)
|
|
196
|
+
)
|
|
197
|
+
fail(
|
|
198
|
+
expr,
|
|
199
|
+
"invalid date",
|
|
200
|
+
"write a calendar date or a timestamp with an explicit offset",
|
|
201
|
+
);
|
|
202
|
+
return new Date(n).toISOString();
|
|
203
|
+
}
|
|
204
|
+
if (expr.kind === "text") return expr.value;
|
|
205
|
+
if (expr.kind === "name" && ["true", "false"].includes(expr.value))
|
|
206
|
+
return expr.value === "true";
|
|
207
|
+
return fail(expr, "expected a literal", "write a typed constant");
|
|
92
208
|
}
|
|
93
209
|
|
|
94
210
|
export function compile(
|
|
95
211
|
source: string,
|
|
96
212
|
options: CompileOptions = {},
|
|
97
213
|
): CompileResult {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// made a 128 KB file cost 2.4 seconds of coordinate arithmetic.
|
|
102
|
-
const lines = lineIndex(source);
|
|
103
|
-
const byteOffsets = utf8OffsetIndex(source);
|
|
104
|
-
const at = (
|
|
214
|
+
const parsed = parseProgram(source);
|
|
215
|
+
const diagnostic = (
|
|
216
|
+
d: Diagnostic,
|
|
105
217
|
stage: CompileDiagnostic["stage"],
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
span
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
218
|
+
): CompileDiagnostic => ({
|
|
219
|
+
...d,
|
|
220
|
+
...lineColAt(source, d.span.start),
|
|
221
|
+
severity: "error",
|
|
222
|
+
stage,
|
|
223
|
+
});
|
|
224
|
+
if (parsed.diagnostics.length)
|
|
225
|
+
return {
|
|
226
|
+
verdict: "invalid",
|
|
227
|
+
diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
|
|
228
|
+
};
|
|
229
|
+
const program = parsed.program;
|
|
230
|
+
try {
|
|
231
|
+
if (program.header)
|
|
232
|
+
fail(
|
|
233
|
+
program,
|
|
234
|
+
"compile a program, not a header",
|
|
235
|
+
'start with program product_name "Title"',
|
|
236
|
+
);
|
|
237
|
+
if (program.currency !== "SAR")
|
|
238
|
+
fail(
|
|
239
|
+
program,
|
|
240
|
+
"this release supports SAR",
|
|
241
|
+
"write currency SAR or omit currency",
|
|
242
|
+
);
|
|
243
|
+
const templates = new Map<string, InstrumentDecl>();
|
|
244
|
+
const used = new Set<string>();
|
|
245
|
+
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
246
|
+
if (used.has(use.name))
|
|
247
|
+
fail(
|
|
248
|
+
use,
|
|
249
|
+
`header ${use.name} is used twice`,
|
|
250
|
+
"remove the duplicate use line",
|
|
251
|
+
);
|
|
252
|
+
used.add(use.name);
|
|
253
|
+
const content = (
|
|
254
|
+
options.standardLibrary ?? bundledStandardLibrary
|
|
255
|
+
).source(use.name);
|
|
256
|
+
if (!content)
|
|
257
|
+
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
258
|
+
const header = parseProgram(content);
|
|
259
|
+
if (
|
|
260
|
+
header.diagnostics.length ||
|
|
261
|
+
!header.program.header ||
|
|
262
|
+
header.program.name !== use.name
|
|
263
|
+
)
|
|
264
|
+
fail(
|
|
265
|
+
use,
|
|
266
|
+
`header ${use.name} is malformed`,
|
|
267
|
+
"repair the header source before compiling",
|
|
268
|
+
);
|
|
269
|
+
for (const decl of header.program.decls)
|
|
270
|
+
if (decl.kind === "instrument")
|
|
271
|
+
templates.set(`${use.name}.${decl.name}`, decl);
|
|
272
|
+
}
|
|
273
|
+
const document: UdlDocument = {
|
|
274
|
+
udl: 3,
|
|
275
|
+
version: 1,
|
|
276
|
+
product: program.name,
|
|
277
|
+
title: program.title,
|
|
278
|
+
currency: "SAR",
|
|
279
|
+
parties: {
|
|
280
|
+
programOperator: { kind: "business", role: "program_operator" },
|
|
281
|
+
programTax: { kind: "business", role: "tax_payable" },
|
|
282
|
+
programFines: { kind: "business", role: "fine_payable" },
|
|
283
|
+
programCosts: { kind: "business", role: "cost_recovery" },
|
|
284
|
+
},
|
|
285
|
+
instruments: [],
|
|
286
|
+
};
|
|
287
|
+
const objects = new Map<string, ObjectDecl>();
|
|
288
|
+
const names = new Set<string>();
|
|
289
|
+
for (const decl of program.decls) {
|
|
290
|
+
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
291
|
+
continue;
|
|
292
|
+
if (names.has(decl.name))
|
|
293
|
+
fail(
|
|
294
|
+
decl,
|
|
295
|
+
`duplicate declaration ${decl.name}`,
|
|
296
|
+
"give this declaration a distinct name",
|
|
297
|
+
);
|
|
298
|
+
names.add(decl.name);
|
|
299
|
+
if (decl.kind === "object") objects.set(decl.name, decl);
|
|
300
|
+
if (decl.kind === "party") {
|
|
301
|
+
if (!["person", "business", "staff"].includes(decl.partyKind))
|
|
302
|
+
fail(
|
|
303
|
+
decl,
|
|
304
|
+
`unknown party kind ${decl.partyKind}`,
|
|
305
|
+
"choose person, business, or staff",
|
|
306
|
+
);
|
|
307
|
+
document.parties[decl.name] = {
|
|
308
|
+
kind: decl.partyKind as "person" | "business" | "staff",
|
|
309
|
+
...(decl.role ? { role: decl.role } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const origins: CompileOriginMapEntry[] = [];
|
|
314
|
+
const materialApprovals = new Set<UdlAction>();
|
|
315
|
+
const implicitDecisions = new Map<
|
|
316
|
+
string,
|
|
317
|
+
{ target: string; action: string; party: string; origin: Span }
|
|
318
|
+
>();
|
|
319
|
+
const addInstrument = (
|
|
320
|
+
decl: InstrumentDecl,
|
|
321
|
+
id: string,
|
|
322
|
+
arguments_: BlockExpr,
|
|
323
|
+
origin: Span,
|
|
324
|
+
inherited = new Map<string, Expr>(),
|
|
325
|
+
inheritedApprovers = new Set<string>(),
|
|
326
|
+
inheritedEnums = new Map<string, string[]>(),
|
|
327
|
+
) => {
|
|
328
|
+
const enums = new Map(inheritedEnums);
|
|
329
|
+
for (const parameter of decl.parameters) {
|
|
330
|
+
const type =
|
|
331
|
+
parameter.value.kind === "default"
|
|
332
|
+
? parameter.value.type
|
|
333
|
+
: parameter.value;
|
|
334
|
+
if (type.kind === "call" && type.name === "enum")
|
|
335
|
+
enums.set(parameter.key, type.args.map(text));
|
|
336
|
+
}
|
|
337
|
+
const approvers = new Set(inheritedApprovers);
|
|
338
|
+
const supplied = entries(arguments_);
|
|
339
|
+
const environment = new Map<string, Expr>(inherited);
|
|
340
|
+
for (const param of decl.parameters) {
|
|
341
|
+
const type =
|
|
342
|
+
param.value.kind === "default" ? param.value.type : param.value;
|
|
343
|
+
const fallback =
|
|
344
|
+
param.value.kind === "default" ? param.value.value : undefined;
|
|
345
|
+
const actual = supplied.get(param.key) ?? fallback;
|
|
346
|
+
if (!actual) {
|
|
347
|
+
if (type.kind === "type" && type.optional) continue;
|
|
348
|
+
fail(
|
|
349
|
+
{ span: origin },
|
|
350
|
+
`${id} needs ${param.key}`,
|
|
351
|
+
`add ${param.key}: value inside ${id}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
environment.set(param.key, actual);
|
|
355
|
+
}
|
|
356
|
+
for (const key of supplied.keys())
|
|
357
|
+
if (!decl.parameters.some((p) => p.key === key))
|
|
358
|
+
fail(
|
|
359
|
+
supplied.get(key)!,
|
|
360
|
+
`unknown tunable ${key}`,
|
|
361
|
+
`choose ${decl.parameters.map((p) => p.key).join(", ")}`,
|
|
362
|
+
);
|
|
363
|
+
const resolve = (expr: Expr, seen = new Set<string>()): Expr => {
|
|
364
|
+
if (
|
|
365
|
+
expr.kind === "call" &&
|
|
366
|
+
["object", "all", "party"].includes(expr.name)
|
|
367
|
+
) {
|
|
368
|
+
if (expr.args.length !== 1)
|
|
369
|
+
fail(
|
|
370
|
+
expr,
|
|
371
|
+
`${expr.name} needs one type`,
|
|
372
|
+
"supply one declared object or party kind",
|
|
373
|
+
);
|
|
374
|
+
const type = text(expr.args[0]!);
|
|
375
|
+
const matches =
|
|
376
|
+
expr.name === "party"
|
|
377
|
+
? Object.entries(document.parties)
|
|
378
|
+
.filter(([, party]) => party.kind === type)
|
|
379
|
+
.map(([name]) => name)
|
|
380
|
+
: [...objects.values()]
|
|
381
|
+
.filter(
|
|
382
|
+
(object) =>
|
|
383
|
+
object.object === type ||
|
|
384
|
+
type.startsWith(`${object.object}.`),
|
|
385
|
+
)
|
|
386
|
+
.map(
|
|
387
|
+
(object) =>
|
|
388
|
+
object.name +
|
|
389
|
+
type.slice(object.object.length).replaceAll(".", "_"),
|
|
390
|
+
);
|
|
391
|
+
if (expr.name !== "all" && matches.length !== 1)
|
|
392
|
+
fail(
|
|
393
|
+
{ span: origin },
|
|
394
|
+
`${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
|
|
395
|
+
"declare the required object or supply this tunable explicitly",
|
|
396
|
+
);
|
|
397
|
+
const items: Expr[] = matches.map((value) => ({
|
|
398
|
+
kind: "name",
|
|
399
|
+
value,
|
|
400
|
+
span: origin,
|
|
401
|
+
}));
|
|
402
|
+
return expr.name === "all"
|
|
403
|
+
? { kind: "list", items, span: origin }
|
|
404
|
+
: items[0]!;
|
|
405
|
+
}
|
|
406
|
+
if (expr.kind !== "name") return expr;
|
|
407
|
+
if (expr.value.startsWith("party.")) {
|
|
408
|
+
const binding = environment.get(expr.value.slice(6));
|
|
409
|
+
if (binding?.kind === "name" && document.parties[binding.value])
|
|
410
|
+
return { ...expr, value: `party.${binding.value}` };
|
|
411
|
+
}
|
|
412
|
+
const [root, ...tail] = expr.value.split(".");
|
|
413
|
+
const bound = environment.get(root!);
|
|
414
|
+
if (!bound || (bound.kind === "name" && bound.value === root))
|
|
415
|
+
return expr;
|
|
416
|
+
if (seen.has(root!))
|
|
417
|
+
return fail(
|
|
418
|
+
expr,
|
|
419
|
+
`cyclic tunable ${root}`,
|
|
420
|
+
"replace the cycle with a literal or declared reference",
|
|
421
|
+
);
|
|
422
|
+
let resolved =
|
|
423
|
+
supplied.has(root!) || inherited.has(root!) || enums.has(root!)
|
|
424
|
+
? bound
|
|
425
|
+
: resolve(bound, new Set([...seen, root!]));
|
|
426
|
+
for (const key of tail) {
|
|
427
|
+
if (resolved.kind !== "block") return expr;
|
|
428
|
+
const child = entries(resolved).get(key);
|
|
429
|
+
if (!child)
|
|
430
|
+
return fail(
|
|
431
|
+
expr,
|
|
432
|
+
`missing tunable ${expr.value}`,
|
|
433
|
+
`declare ${key} in ${root}`,
|
|
434
|
+
);
|
|
435
|
+
resolved = resolve(child, new Set([...seen, root!]));
|
|
436
|
+
}
|
|
437
|
+
return resolved;
|
|
438
|
+
};
|
|
439
|
+
for (const param of decl.parameters) {
|
|
440
|
+
const actual = environment.get(param.key);
|
|
441
|
+
if (!actual) continue;
|
|
442
|
+
const t =
|
|
443
|
+
param.value.kind === "default" ? param.value.type : param.value;
|
|
444
|
+
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
445
|
+
const v =
|
|
446
|
+
supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
|
|
447
|
+
environment.set(param.key, v);
|
|
448
|
+
if (type === "enum" && t.kind === "call") {
|
|
449
|
+
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
450
|
+
fail(
|
|
451
|
+
v,
|
|
452
|
+
`invalid ${param.key}`,
|
|
453
|
+
`choose ${t.args.map(text).join(", ")}`,
|
|
454
|
+
);
|
|
455
|
+
} else if (type === "list") {
|
|
456
|
+
if (v.kind !== "list")
|
|
457
|
+
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
458
|
+
} else if (type === "party" || type === "approval") {
|
|
459
|
+
if (v.kind !== "name" || !document.parties[v.value])
|
|
460
|
+
fail(
|
|
461
|
+
v,
|
|
462
|
+
`${param.key} needs a declared party`,
|
|
463
|
+
"declare a party and use its name here",
|
|
464
|
+
);
|
|
465
|
+
if (type === "approval") approvers.add(text(v));
|
|
466
|
+
} else if (type === "ref") {
|
|
467
|
+
const values = v.kind === "list" ? v.items : [v];
|
|
468
|
+
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
469
|
+
fail(
|
|
470
|
+
v,
|
|
471
|
+
`${param.key} accepts one reference`,
|
|
472
|
+
"use one object name",
|
|
473
|
+
);
|
|
474
|
+
if (!values.length || values.length > 16)
|
|
475
|
+
fail(
|
|
476
|
+
v,
|
|
477
|
+
"reference union needs 1 to 16 objects",
|
|
478
|
+
"use at most 16 distinct object names",
|
|
479
|
+
);
|
|
480
|
+
const seen = new Set<string>();
|
|
481
|
+
for (const value of values) {
|
|
482
|
+
if (value.kind !== "name")
|
|
483
|
+
fail(
|
|
484
|
+
value,
|
|
485
|
+
"reference needs an object name",
|
|
486
|
+
"name a declared object",
|
|
487
|
+
);
|
|
488
|
+
const [root, ...tail] = value.value.split(".");
|
|
489
|
+
const obj = objects.get(root!);
|
|
490
|
+
if (
|
|
491
|
+
(!obj &&
|
|
492
|
+
!document.instruments.some(
|
|
493
|
+
(inst) => inst.id === value.value,
|
|
494
|
+
)) ||
|
|
495
|
+
(t.kind === "type" &&
|
|
496
|
+
t.target &&
|
|
497
|
+
[obj?.object, ...tail].join(".") !== t.target)
|
|
498
|
+
)
|
|
499
|
+
fail(
|
|
500
|
+
value,
|
|
501
|
+
`${param.key} has the wrong object type`,
|
|
502
|
+
`use an object of type ${t.kind === "type" ? t.target : "ref"}`,
|
|
503
|
+
);
|
|
504
|
+
if (seen.has(value.value))
|
|
505
|
+
fail(value, "duplicate reference", "list each object once");
|
|
506
|
+
seen.add(value.value);
|
|
507
|
+
}
|
|
508
|
+
} else if (type === "fee" || type === "split" || type === "policy") {
|
|
509
|
+
if (v.kind !== "block")
|
|
510
|
+
fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
|
|
511
|
+
} else if (
|
|
512
|
+
type === "money" ||
|
|
513
|
+
type === "percent" ||
|
|
514
|
+
type === "date" ||
|
|
515
|
+
type === "duration" ||
|
|
516
|
+
type === "integer" ||
|
|
517
|
+
type === "text"
|
|
518
|
+
) {
|
|
519
|
+
if (v.kind === "name" && v.value === "runtime") continue;
|
|
520
|
+
const expected = type === "integer" ? "number" : type;
|
|
521
|
+
if (
|
|
522
|
+
v.kind !== expected &&
|
|
523
|
+
!(
|
|
524
|
+
type === "duration" &&
|
|
525
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
526
|
+
/^P/.test(v.value)
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
|
|
530
|
+
try {
|
|
531
|
+
const value = literal(v);
|
|
532
|
+
const bounds = tunableBounds(t);
|
|
533
|
+
if (
|
|
534
|
+
bounds &&
|
|
535
|
+
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
536
|
+
BigInt(String(value)) > BigInt(bounds.maximum))
|
|
537
|
+
)
|
|
538
|
+
fail(
|
|
539
|
+
v,
|
|
540
|
+
`${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
|
|
541
|
+
"choose a value inside the tunable's bounds",
|
|
542
|
+
);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof CompileFailure)
|
|
545
|
+
fail(
|
|
546
|
+
v,
|
|
547
|
+
`${param.key}: ${error.diagnostic.message}`,
|
|
548
|
+
error.diagnostic.fix,
|
|
549
|
+
);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const body = entries(decl.body);
|
|
555
|
+
for (const constraint of asBlock(body.get("constraints")).entries) {
|
|
556
|
+
const rule = constraint.value;
|
|
557
|
+
if (
|
|
558
|
+
rule.kind !== "call" ||
|
|
559
|
+
rule.args.length !== 1 ||
|
|
560
|
+
!["less_than", "at_most", "greater_than"].includes(rule.name)
|
|
561
|
+
)
|
|
562
|
+
fail(
|
|
563
|
+
rule,
|
|
564
|
+
"invalid tunable constraint",
|
|
565
|
+
"use less_than(name), at_most(name), or greater_than(name)",
|
|
566
|
+
);
|
|
567
|
+
const left = environment.get(constraint.key),
|
|
568
|
+
right = environment.get(text(rule.args[0]!));
|
|
569
|
+
if (!left || !right)
|
|
570
|
+
fail(
|
|
571
|
+
rule,
|
|
572
|
+
"constraint needs two declared tunables",
|
|
573
|
+
"name the compared tunables",
|
|
574
|
+
);
|
|
575
|
+
const a = BigInt(String(literal(left))),
|
|
576
|
+
b = BigInt(String(literal(right)));
|
|
577
|
+
if (
|
|
578
|
+
!(rule.name === "less_than"
|
|
579
|
+
? a < b
|
|
580
|
+
: rule.name === "at_most"
|
|
581
|
+
? a <= b
|
|
582
|
+
: a > b)
|
|
583
|
+
)
|
|
584
|
+
fail(
|
|
585
|
+
left,
|
|
586
|
+
`${constraint.key} must be ${rule.name} ${text(rule.args[0]!)}`,
|
|
587
|
+
"choose values satisfying the header constraint",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
for (const key of body.keys())
|
|
591
|
+
if (
|
|
592
|
+
![
|
|
593
|
+
"fields",
|
|
594
|
+
"lifecycle",
|
|
595
|
+
"records",
|
|
596
|
+
"summary",
|
|
597
|
+
"invariants",
|
|
598
|
+
"constraints",
|
|
599
|
+
].includes(key) &&
|
|
600
|
+
!key.startsWith("action ")
|
|
601
|
+
)
|
|
602
|
+
fail(
|
|
603
|
+
decl,
|
|
604
|
+
`unknown instrument clause ${key}`,
|
|
605
|
+
"use fields, lifecycle, actions, invariants, or records",
|
|
606
|
+
);
|
|
607
|
+
const records = entries(asBlock(body.get("records")));
|
|
608
|
+
if (!inherited.size)
|
|
609
|
+
environment.set("parent", { kind: "name", value: id, span: origin });
|
|
610
|
+
for (const key of records.keys())
|
|
611
|
+
if (!decl.parameters.some((parameter) => parameter.key === key))
|
|
612
|
+
environment.set(key, {
|
|
613
|
+
kind: "name",
|
|
614
|
+
value: `${id}_${key}`,
|
|
615
|
+
span: origin,
|
|
616
|
+
});
|
|
617
|
+
const fields: UdlField[] = [];
|
|
618
|
+
const calculations: UdlCalculation[] = [];
|
|
619
|
+
const path = (expr: Expr): string => {
|
|
620
|
+
const value = resolve(expr);
|
|
621
|
+
const name = text(value);
|
|
622
|
+
if (document.parties[name]) return `party.${name}`;
|
|
623
|
+
return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
|
|
624
|
+
};
|
|
625
|
+
const val = (expr: Expr): UdlValue => {
|
|
626
|
+
const v = resolve(expr);
|
|
627
|
+
return v.kind === "name" && !["true", "false"].includes(v.value)
|
|
628
|
+
? { field: path(v) }
|
|
629
|
+
: { literal: literal(v) };
|
|
630
|
+
};
|
|
631
|
+
const data = (expr: Expr): unknown => {
|
|
632
|
+
if (
|
|
633
|
+
expr.kind === "call" &&
|
|
634
|
+
expr.name === "current" &&
|
|
635
|
+
!expr.args.length
|
|
636
|
+
)
|
|
637
|
+
return id;
|
|
638
|
+
const value = resolve(expr);
|
|
639
|
+
if (value.kind === "block") {
|
|
640
|
+
const result = Object.fromEntries(
|
|
641
|
+
[...entries(value)].map(([key, value]) => [key, data(value)]),
|
|
642
|
+
);
|
|
643
|
+
const selection = result.selection as
|
|
644
|
+
| { instrument?: unknown }
|
|
645
|
+
| undefined;
|
|
646
|
+
if (
|
|
647
|
+
Array.isArray(selection?.instrument) &&
|
|
648
|
+
selection.instrument.length === 0
|
|
649
|
+
) {
|
|
650
|
+
if (result.op === "aggregate")
|
|
651
|
+
return {
|
|
652
|
+
target: result.target,
|
|
653
|
+
op: "sum",
|
|
654
|
+
values: [{ literal: result.measure === "count" ? 0 : "0" }],
|
|
655
|
+
};
|
|
656
|
+
if (result.kind === "aggregate")
|
|
657
|
+
return {
|
|
658
|
+
kind: "compare",
|
|
659
|
+
left: { literal: result.measure === "count" ? 0 : "0" },
|
|
660
|
+
operator: result.operator,
|
|
661
|
+
right: result.value,
|
|
662
|
+
};
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
if (value.kind === "list")
|
|
668
|
+
return value.items.map(data).filter((item) => item !== null);
|
|
669
|
+
if (
|
|
670
|
+
value.kind === "call" &&
|
|
671
|
+
value.name === "child" &&
|
|
672
|
+
value.args.length === 2
|
|
673
|
+
) {
|
|
674
|
+
const parent = resolve(value.args[0]!);
|
|
675
|
+
const child = (item: Expr) =>
|
|
676
|
+
`${text(item).replaceAll(".", "_")}_${text(value.args[1]!)}`;
|
|
677
|
+
return parent.kind === "list"
|
|
678
|
+
? parent.items.map(child)
|
|
679
|
+
: child(parent);
|
|
680
|
+
}
|
|
681
|
+
if (value.kind === "name")
|
|
682
|
+
return value.value === "true" || value.value === "false"
|
|
683
|
+
? value.value === "true"
|
|
684
|
+
: value.value;
|
|
685
|
+
return literal(value);
|
|
686
|
+
};
|
|
687
|
+
const lowerFields = (block: BlockExpr): UdlField[] => {
|
|
688
|
+
const result: UdlField[] = [];
|
|
689
|
+
for (const row of block.entries) {
|
|
690
|
+
const t = row.value.kind === "default" ? row.value.type : row.value;
|
|
691
|
+
const constant =
|
|
692
|
+
row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
693
|
+
const type =
|
|
694
|
+
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
695
|
+
const f: Record<string, unknown> = {
|
|
696
|
+
name: row.key,
|
|
697
|
+
type,
|
|
698
|
+
...(t.kind === "type" && t.optional ? { optional: true } : {}),
|
|
699
|
+
};
|
|
700
|
+
if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
|
|
701
|
+
if (["integer", "money"].includes(type) && t.kind === "call") {
|
|
702
|
+
if (t.args.length !== 2)
|
|
703
|
+
fail(
|
|
704
|
+
t,
|
|
705
|
+
"bounded fields need a minimum and maximum",
|
|
706
|
+
"write integer(1, 12) or money(0 SAR, 100 SAR)",
|
|
707
|
+
);
|
|
708
|
+
f.minimum = literal(resolve(t.args[0]!));
|
|
709
|
+
f.maximum = literal(resolve(t.args[1]!));
|
|
710
|
+
}
|
|
711
|
+
if (type === "list" && t.kind === "call") {
|
|
712
|
+
f.item = text(t.args[0]!);
|
|
713
|
+
f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
|
|
714
|
+
}
|
|
715
|
+
if (type === "list" && t.kind === "type") {
|
|
716
|
+
f.item = t.target;
|
|
717
|
+
f.maxItems = 366;
|
|
718
|
+
}
|
|
719
|
+
if (type === "ref") {
|
|
720
|
+
const target = t.kind === "type" ? t.target : undefined;
|
|
721
|
+
if (!target)
|
|
722
|
+
fail(row, "reference needs a target", "write ref<object>");
|
|
723
|
+
const [root, ...tail] = target.split(".");
|
|
724
|
+
const resolved = environment.get(root!);
|
|
725
|
+
const resolvedTargets =
|
|
726
|
+
resolved?.kind === "list"
|
|
727
|
+
? resolved.items
|
|
728
|
+
: resolved
|
|
729
|
+
? [resolved]
|
|
730
|
+
: [];
|
|
731
|
+
const targets = resolvedTargets.map((value) =>
|
|
732
|
+
[text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
|
|
733
|
+
);
|
|
734
|
+
f.target =
|
|
735
|
+
target === "self"
|
|
736
|
+
? id
|
|
737
|
+
: targets.length === 1
|
|
738
|
+
? targets[0]
|
|
739
|
+
: targets.length
|
|
740
|
+
? targets
|
|
741
|
+
: target;
|
|
742
|
+
} else if (type === "account") {
|
|
743
|
+
if (t.kind === "call") {
|
|
744
|
+
if (t.args.length < 1 || t.args.length > 4)
|
|
745
|
+
fail(
|
|
746
|
+
t,
|
|
747
|
+
"account needs an owner, optional book, mode and key",
|
|
748
|
+
"write account(buyer, claim, contra)",
|
|
749
|
+
);
|
|
750
|
+
f.owner = text(resolve(t.args[0]!));
|
|
751
|
+
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
752
|
+
if (t.args[2]) {
|
|
753
|
+
const mode = text(t.args[2]);
|
|
754
|
+
if (["contra", "external"].includes(mode)) f[mode] = true;
|
|
755
|
+
else f.key = mode;
|
|
756
|
+
}
|
|
757
|
+
if (t.args[3]) f.key = text(t.args[3]);
|
|
758
|
+
} else if (t.kind === "type" && t.owner) {
|
|
759
|
+
f.owner =
|
|
760
|
+
t.owner === "self"
|
|
761
|
+
? "self"
|
|
762
|
+
: text(
|
|
763
|
+
resolve({ kind: "name", value: t.owner, span: row.span }),
|
|
764
|
+
);
|
|
765
|
+
f.book = "cash";
|
|
766
|
+
} else
|
|
767
|
+
fail(
|
|
768
|
+
row,
|
|
769
|
+
"account needs an owner",
|
|
770
|
+
"write account of buyer or account of self",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (
|
|
774
|
+
constant &&
|
|
775
|
+
!(constant.kind === "name" && constant.value === "runtime")
|
|
776
|
+
) {
|
|
777
|
+
if (constant.kind === "call") {
|
|
778
|
+
const args =
|
|
779
|
+
constant.name === "aggregate" ? [] : constant.args.map(val);
|
|
780
|
+
const target = row.key;
|
|
781
|
+
if (constant.name === "aggregate" && constant.args.length === 2)
|
|
782
|
+
calculations.push({
|
|
783
|
+
target,
|
|
784
|
+
op: "aggregate",
|
|
785
|
+
selection: data(
|
|
786
|
+
constant.args[0]!,
|
|
787
|
+
) as import("@hyperscale0/udl").UdlSelection,
|
|
788
|
+
measure:
|
|
789
|
+
text(constant.args[1]!) === "count"
|
|
790
|
+
? "count"
|
|
791
|
+
: { sum: text(constant.args[1]!) },
|
|
792
|
+
});
|
|
793
|
+
else if (constant.name === "at" && args.length === 2)
|
|
794
|
+
calculations.push({
|
|
795
|
+
target,
|
|
796
|
+
op: "at",
|
|
797
|
+
list: path(constant.args[0]!),
|
|
798
|
+
position: args[1]!,
|
|
799
|
+
});
|
|
800
|
+
else if (constant.name === "ratio" && args.length === 3)
|
|
801
|
+
calculations.push({
|
|
802
|
+
target,
|
|
803
|
+
op: "ratio",
|
|
804
|
+
amount: args[0]!,
|
|
805
|
+
numerator: args[1]!,
|
|
806
|
+
denominator: args[2]!,
|
|
807
|
+
rounding: "floor",
|
|
808
|
+
});
|
|
809
|
+
else if (constant.name === "rate" && args.length === 2)
|
|
810
|
+
calculations.push({
|
|
811
|
+
target,
|
|
812
|
+
op: "rate",
|
|
813
|
+
base: args[0]!,
|
|
814
|
+
bps: args[1]!,
|
|
815
|
+
rounding: "floor",
|
|
816
|
+
});
|
|
817
|
+
else if (constant.name === "sum" || constant.name === "minimum")
|
|
818
|
+
calculations.push({ target, op: constant.name, values: args });
|
|
819
|
+
else if (constant.name === "subtract" && args.length >= 2)
|
|
820
|
+
calculations.push({
|
|
821
|
+
target,
|
|
822
|
+
op: "subtract",
|
|
823
|
+
base: args[0]!,
|
|
824
|
+
subtract: args.slice(1),
|
|
825
|
+
});
|
|
826
|
+
else if (constant.name === "multiply" && args.length === 2)
|
|
827
|
+
calculations.push({
|
|
828
|
+
target,
|
|
829
|
+
op: "multiply",
|
|
830
|
+
amount: args[0]!,
|
|
831
|
+
units: args[1]!,
|
|
832
|
+
});
|
|
833
|
+
else if (constant.name === "divide" && args.length === 2)
|
|
834
|
+
calculations.push({
|
|
835
|
+
target,
|
|
836
|
+
op: "divide",
|
|
837
|
+
amount: args[0]!,
|
|
838
|
+
divisor: args[1]!,
|
|
839
|
+
rounding: "floor",
|
|
840
|
+
});
|
|
841
|
+
else if (
|
|
842
|
+
["before", "after"].includes(constant.name) &&
|
|
843
|
+
args.length === 2
|
|
844
|
+
)
|
|
845
|
+
calculations.push({
|
|
846
|
+
target,
|
|
847
|
+
op: "shift",
|
|
848
|
+
date: args[0]!,
|
|
849
|
+
milliseconds: args[1]!,
|
|
850
|
+
direction: constant.name as "before" | "after",
|
|
851
|
+
});
|
|
852
|
+
else
|
|
853
|
+
fail(
|
|
854
|
+
constant,
|
|
855
|
+
`unknown calculation ${constant.name}`,
|
|
856
|
+
"use rate, sum, minimum, subtract, multiply, divide, before or after",
|
|
857
|
+
);
|
|
858
|
+
} else if (type === "enum" && constant.kind === "name")
|
|
859
|
+
f.value = constant.value;
|
|
860
|
+
else if (type === "money" && constant.kind === "name")
|
|
861
|
+
calculations.push({
|
|
862
|
+
target: row.key,
|
|
863
|
+
op: "sum",
|
|
864
|
+
values: [val(constant)],
|
|
865
|
+
});
|
|
866
|
+
else f.value = literal(constant);
|
|
867
|
+
}
|
|
868
|
+
result.push(f as UdlField);
|
|
869
|
+
}
|
|
870
|
+
return result;
|
|
871
|
+
};
|
|
872
|
+
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
873
|
+
const lifecycle = data(
|
|
874
|
+
body.get("lifecycle") ?? emptyBlock,
|
|
875
|
+
) as UdlInstrument["lifecycle"];
|
|
876
|
+
lifecycle.transitions = {};
|
|
877
|
+
const inst: UdlInstrument = {
|
|
878
|
+
id,
|
|
879
|
+
title: title(id),
|
|
880
|
+
summary: body.has("summary")
|
|
881
|
+
? String(data(body.get("summary")!))
|
|
882
|
+
: title(id),
|
|
883
|
+
fields,
|
|
884
|
+
calculate: calculations,
|
|
885
|
+
lifecycle,
|
|
886
|
+
actions: {},
|
|
887
|
+
actionOrder: [],
|
|
888
|
+
};
|
|
889
|
+
for (const row of decl.body.entries.filter((e) =>
|
|
890
|
+
e.key.startsWith("action "),
|
|
891
|
+
)) {
|
|
892
|
+
const name = row.key.slice(7);
|
|
893
|
+
const selected = (block: BlockExpr): BlockExpr => ({
|
|
894
|
+
...block,
|
|
895
|
+
entries: block.entries.flatMap((entry) => {
|
|
896
|
+
if (!entry.key.startsWith("when ")) return [entry];
|
|
897
|
+
const [, tunable, , choice] = entry.key.split(" ");
|
|
898
|
+
const binding = environment.get(tunable!);
|
|
899
|
+
if (!binding)
|
|
900
|
+
fail(
|
|
901
|
+
entry,
|
|
902
|
+
`unknown branch tunable ${tunable}`,
|
|
903
|
+
"name an enum tunable declared by this header",
|
|
904
|
+
);
|
|
905
|
+
if (!enums.get(tunable!)?.includes(choice!))
|
|
906
|
+
fail(
|
|
907
|
+
entry,
|
|
908
|
+
`unknown enum branch ${choice}`,
|
|
909
|
+
"use a declared enum value",
|
|
910
|
+
);
|
|
911
|
+
const body = asBlock(entry.value);
|
|
912
|
+
for (const clause of body.entries)
|
|
913
|
+
if (
|
|
914
|
+
!["requires", "moves", "invoke", "calculate"].includes(
|
|
915
|
+
clause.key,
|
|
916
|
+
) &&
|
|
917
|
+
!clause.key.startsWith("when ")
|
|
918
|
+
)
|
|
919
|
+
fail(
|
|
920
|
+
clause,
|
|
921
|
+
"when permits requirements, calculations, moves and invocations",
|
|
922
|
+
"keep lifecycle and actor clauses outside the branch",
|
|
923
|
+
);
|
|
924
|
+
return text(binding) === choice ? selected(body).entries : [];
|
|
128
925
|
}),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
926
|
+
});
|
|
927
|
+
const slots = entries(selected(asBlock(row.value)));
|
|
928
|
+
const a: UdlAction = {
|
|
929
|
+
summary: slots.has("summary")
|
|
930
|
+
? String(data(slots.get("summary")!))
|
|
931
|
+
: `${title(name)} ${id}`,
|
|
932
|
+
publicAction: camel(`${name}_${id}`),
|
|
933
|
+
event: `${id}.${name}`,
|
|
934
|
+
actor: "caller",
|
|
935
|
+
input: lowerFields(asBlock(slots.get("input"))),
|
|
936
|
+
requires: [],
|
|
937
|
+
moves: [],
|
|
938
|
+
};
|
|
939
|
+
if (name !== "create") {
|
|
940
|
+
const from = slots.get("from");
|
|
941
|
+
const to = slots.get("to");
|
|
942
|
+
if (!from || !to)
|
|
943
|
+
fail(
|
|
944
|
+
row,
|
|
945
|
+
`action ${name} needs from and to`,
|
|
946
|
+
"declare its source and destination states",
|
|
947
|
+
);
|
|
948
|
+
lifecycle.transitions[name] = {
|
|
949
|
+
from: (from!.kind === "list" ? from!.items : [from!]).map((e) =>
|
|
950
|
+
String(data(e)),
|
|
951
|
+
),
|
|
952
|
+
to: String(data(to!)),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
for (const [key, expr] of slots) {
|
|
956
|
+
if (["from", "to", "input", "summary"].includes(key)) continue;
|
|
957
|
+
if (key === "moves") {
|
|
958
|
+
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
959
|
+
for (const [index, move] of moves.entries()) {
|
|
960
|
+
const parts = entries(asBlock(move));
|
|
961
|
+
const op = parts.has("operation")
|
|
962
|
+
? String(data(parts.get("operation")!))
|
|
963
|
+
: "internal_transfer.create";
|
|
964
|
+
if (op === "internal_transfer.reserve" && !parts.has("capture"))
|
|
965
|
+
fail(
|
|
966
|
+
move,
|
|
967
|
+
"reserve needs a receipt field",
|
|
968
|
+
"add capture: receipt_name",
|
|
969
|
+
);
|
|
970
|
+
if (
|
|
971
|
+
(op === "internal_transfer.post" ||
|
|
972
|
+
op === "internal_transfer.void") &&
|
|
973
|
+
!parts.has("transfer")
|
|
974
|
+
)
|
|
975
|
+
fail(
|
|
976
|
+
move,
|
|
977
|
+
"post and void need a receipt",
|
|
978
|
+
"add transfer: receipt_name",
|
|
979
|
+
);
|
|
980
|
+
const key = parts.has("key")
|
|
981
|
+
? String(data(parts.get("key")!))
|
|
982
|
+
: `move${index + 1}`;
|
|
983
|
+
if (
|
|
984
|
+
op === "internal_transfer.create" ||
|
|
985
|
+
op === "internal_transfer.reserve"
|
|
986
|
+
) {
|
|
987
|
+
const amount = parts.get("amount"),
|
|
988
|
+
from = parts.get("from"),
|
|
989
|
+
to = parts.get("to");
|
|
990
|
+
if (parts.has("shares")) {
|
|
991
|
+
if (
|
|
992
|
+
!amount ||
|
|
993
|
+
!from ||
|
|
994
|
+
op !== "internal_transfer.create" ||
|
|
995
|
+
parts.has("fee")
|
|
996
|
+
)
|
|
997
|
+
fail(
|
|
998
|
+
move,
|
|
999
|
+
"split needs a posted amount and source",
|
|
1000
|
+
"declare amount, from and shares without a fee",
|
|
1001
|
+
);
|
|
1002
|
+
const shares = [
|
|
1003
|
+
...entries(asBlock(resolve(parts.get("shares")!))),
|
|
1004
|
+
];
|
|
1005
|
+
if (!shares.length || shares.length > 64)
|
|
1006
|
+
fail(
|
|
1007
|
+
move,
|
|
1008
|
+
"split needs 1 to 64 recipients",
|
|
1009
|
+
"name the receiving parties and percentages",
|
|
1010
|
+
);
|
|
1011
|
+
let total = 0;
|
|
1012
|
+
const pieces: UdlValue[] = [];
|
|
1013
|
+
for (const [index, [party, rate]] of shares.entries()) {
|
|
1014
|
+
const recipient = resolve({
|
|
1015
|
+
kind: "name",
|
|
1016
|
+
value: party,
|
|
1017
|
+
span: rate.span,
|
|
1018
|
+
});
|
|
1019
|
+
if (
|
|
1020
|
+
recipient.kind !== "name" ||
|
|
1021
|
+
!document.parties[recipient.value] ||
|
|
1022
|
+
rate.kind !== "percent"
|
|
1023
|
+
)
|
|
1024
|
+
fail(
|
|
1025
|
+
rate,
|
|
1026
|
+
"split needs party percentages",
|
|
1027
|
+
"write party_name: 70%",
|
|
1028
|
+
);
|
|
1029
|
+
total += Number(literal(rate));
|
|
1030
|
+
const target = `${name}_${key}_share${index + 1}`;
|
|
1031
|
+
if (fields.some((f) => f.name === target))
|
|
1032
|
+
fail(
|
|
1033
|
+
move,
|
|
1034
|
+
"split field name conflicts",
|
|
1035
|
+
"rename the move key",
|
|
1036
|
+
);
|
|
1037
|
+
fields.push({ name: target, type: "money" });
|
|
1038
|
+
calculations.push(
|
|
1039
|
+
index === shares.length - 1 && pieces.length
|
|
1040
|
+
? {
|
|
1041
|
+
target,
|
|
1042
|
+
op: "subtract",
|
|
1043
|
+
base: val(amount),
|
|
1044
|
+
subtract: [...pieces],
|
|
1045
|
+
}
|
|
1046
|
+
: {
|
|
1047
|
+
target,
|
|
1048
|
+
op: "rate",
|
|
1049
|
+
base: val(amount),
|
|
1050
|
+
bps: { literal: literal(rate) },
|
|
1051
|
+
rounding: "floor",
|
|
1052
|
+
},
|
|
1053
|
+
);
|
|
1054
|
+
const piece: UdlValue = { field: `self.${target}` };
|
|
1055
|
+
pieces.push(piece);
|
|
1056
|
+
a.moves.push({
|
|
1057
|
+
key: `${key}_${index + 1}`,
|
|
1058
|
+
operation: "internal_transfer.create",
|
|
1059
|
+
amount: piece,
|
|
1060
|
+
from: path(from),
|
|
1061
|
+
to: `party.${recipient.value}`,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (total !== 10000)
|
|
1065
|
+
fail(
|
|
1066
|
+
move,
|
|
1067
|
+
"split percentages must sum to 100%",
|
|
1068
|
+
"give the recipients exactly 100% together",
|
|
1069
|
+
);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (!amount || !from || !to)
|
|
1073
|
+
fail(
|
|
1074
|
+
move,
|
|
1075
|
+
"move needs amount, from and to",
|
|
1076
|
+
"declare all three move operands",
|
|
1077
|
+
);
|
|
1078
|
+
const transfer = {
|
|
1079
|
+
key,
|
|
1080
|
+
operation: op,
|
|
1081
|
+
...(parts.has("capture")
|
|
1082
|
+
? { capture: String(data(parts.get("capture")!)) }
|
|
1083
|
+
: {}),
|
|
1084
|
+
amount: val(amount!),
|
|
1085
|
+
from: path(from!),
|
|
1086
|
+
to: path(to!),
|
|
1087
|
+
};
|
|
1088
|
+
const fee = parts.get("fee");
|
|
1089
|
+
if (!fee)
|
|
1090
|
+
a.moves.push(
|
|
1091
|
+
op === "internal_transfer.reserve"
|
|
1092
|
+
? {
|
|
1093
|
+
...transfer,
|
|
1094
|
+
operation: op,
|
|
1095
|
+
capture: String(data(parts.get("capture")!)),
|
|
1096
|
+
}
|
|
1097
|
+
: { ...transfer, operation: op },
|
|
1098
|
+
);
|
|
1099
|
+
else {
|
|
1100
|
+
if (op !== "internal_transfer.create")
|
|
1101
|
+
fail(
|
|
1102
|
+
move,
|
|
1103
|
+
"fees settle with a posted transfer",
|
|
1104
|
+
"declare a separate posted settlement after releasing the reservation",
|
|
1105
|
+
);
|
|
1106
|
+
const terms = entries(asBlock(resolve(fee)));
|
|
1107
|
+
for (const term of terms.keys())
|
|
1108
|
+
if (!["seller", "buyer", "tax"].includes(term))
|
|
1109
|
+
fail(
|
|
1110
|
+
fee,
|
|
1111
|
+
`unknown fee term ${term}`,
|
|
1112
|
+
"choose seller or buyer, and optional tax",
|
|
1113
|
+
);
|
|
1114
|
+
const paidBy = terms.has("seller") ? "seller" : "buyer";
|
|
1115
|
+
if (terms.has("seller") === terms.has("buyer"))
|
|
1116
|
+
fail(
|
|
1117
|
+
fee,
|
|
1118
|
+
"fee needs exactly one paying party",
|
|
1119
|
+
"write seller: 1% or buyer: 1%",
|
|
1120
|
+
);
|
|
1121
|
+
const quoted = terms.get(paidBy)!;
|
|
1122
|
+
const rate = quoted.kind === "capped" ? quoted.rate : quoted;
|
|
1123
|
+
if (rate.kind !== "percent")
|
|
1124
|
+
fail(
|
|
1125
|
+
rate,
|
|
1126
|
+
"fee rate needs a percentage",
|
|
1127
|
+
"write a percentage such as 1%",
|
|
1128
|
+
);
|
|
1129
|
+
const prefix = `${name}_${key}`;
|
|
1130
|
+
const derived = (
|
|
1131
|
+
suffix: string,
|
|
1132
|
+
calculation: Omit<UdlCalculation, "target">,
|
|
1133
|
+
): UdlValue => {
|
|
1134
|
+
const target = prefix + "_" + suffix;
|
|
1135
|
+
if (fields.some((f) => f.name === target))
|
|
1136
|
+
fail(
|
|
1137
|
+
move,
|
|
1138
|
+
`generated fee field ${target} conflicts with a field`,
|
|
1139
|
+
"rename the authored field or move key",
|
|
1140
|
+
);
|
|
1141
|
+
fields.push({ name: target, type: "money" });
|
|
1142
|
+
calculations.push({
|
|
1143
|
+
...calculation,
|
|
1144
|
+
target,
|
|
1145
|
+
} as UdlCalculation);
|
|
1146
|
+
return { field: `self.${target}` };
|
|
1147
|
+
};
|
|
1148
|
+
const gross = derived("feeGross", {
|
|
1149
|
+
op: "rate",
|
|
1150
|
+
base: transfer.amount,
|
|
1151
|
+
bps: { literal: literal(rate) },
|
|
1152
|
+
rounding: "floor",
|
|
1153
|
+
} as Omit<UdlCalculation, "target">);
|
|
1154
|
+
let charge = gross;
|
|
1155
|
+
if (quoted.kind === "capped") {
|
|
1156
|
+
if (quoted.cap.kind !== "money")
|
|
1157
|
+
fail(
|
|
1158
|
+
quoted.cap,
|
|
1159
|
+
"fee cap needs money",
|
|
1160
|
+
"write an amount such as 750 SAR",
|
|
1161
|
+
);
|
|
1162
|
+
charge = derived("fee", {
|
|
1163
|
+
op: "minimum",
|
|
1164
|
+
values: [gross, { literal: literal(quoted.cap) }],
|
|
1165
|
+
} as Omit<UdlCalculation, "target">);
|
|
1166
|
+
}
|
|
1167
|
+
const tax = terms.get("tax");
|
|
1168
|
+
if (tax && tax.kind !== "percent")
|
|
1169
|
+
fail(
|
|
1170
|
+
tax,
|
|
1171
|
+
"tax needs a percentage of the fee",
|
|
1172
|
+
"write tax: 15%",
|
|
1173
|
+
);
|
|
1174
|
+
const vat = tax
|
|
1175
|
+
? derived("tax", {
|
|
1176
|
+
op: "rate",
|
|
1177
|
+
base: charge,
|
|
1178
|
+
bps: { literal: literal(tax) },
|
|
1179
|
+
rounding: "floor",
|
|
1180
|
+
} as Omit<UdlCalculation, "target">)
|
|
1181
|
+
: undefined;
|
|
1182
|
+
const charges = vat ? [charge, vat] : [charge];
|
|
1183
|
+
const net =
|
|
1184
|
+
paidBy === "seller"
|
|
1185
|
+
? derived("net", {
|
|
1186
|
+
op: "subtract",
|
|
1187
|
+
base: transfer.amount,
|
|
1188
|
+
subtract: charges,
|
|
1189
|
+
} as Omit<UdlCalculation, "target">)
|
|
1190
|
+
: transfer.amount;
|
|
1191
|
+
for (const [party, role] of [
|
|
1192
|
+
["programOperator", "program_operator"],
|
|
1193
|
+
["programTax", "tax_payable"],
|
|
1194
|
+
] as const) {
|
|
1195
|
+
if (
|
|
1196
|
+
document.parties[party] &&
|
|
1197
|
+
document.parties[party]!.role !== role
|
|
1198
|
+
)
|
|
1199
|
+
fail(
|
|
1200
|
+
move,
|
|
1201
|
+
`${party} is reserved for fees`,
|
|
1202
|
+
"rename this party",
|
|
1203
|
+
);
|
|
1204
|
+
document.parties[party] = { kind: "business", role };
|
|
1205
|
+
}
|
|
1206
|
+
a.moves.push({
|
|
1207
|
+
...transfer,
|
|
1208
|
+
operation: "internal_transfer.create",
|
|
1209
|
+
amount: net,
|
|
1210
|
+
});
|
|
1211
|
+
a.moves.push({
|
|
1212
|
+
key: key + "Fee",
|
|
1213
|
+
operation: "internal_transfer.create",
|
|
1214
|
+
amount: charge,
|
|
1215
|
+
from: transfer.from,
|
|
1216
|
+
to: "party.programOperator",
|
|
1217
|
+
});
|
|
1218
|
+
if (vat)
|
|
1219
|
+
a.moves.push({
|
|
1220
|
+
key: key + "Tax",
|
|
1221
|
+
operation: "internal_transfer.create",
|
|
1222
|
+
amount: vat,
|
|
1223
|
+
from: transfer.from,
|
|
1224
|
+
to: "party.programTax",
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
} else if (
|
|
1228
|
+
op === "internal_transfer.post" ||
|
|
1229
|
+
op === "internal_transfer.void"
|
|
1230
|
+
)
|
|
1231
|
+
a.moves.push({
|
|
1232
|
+
key,
|
|
1233
|
+
operation: op,
|
|
1234
|
+
transfer: path(parts.get("transfer")!),
|
|
1235
|
+
...(parts.has("capture")
|
|
1236
|
+
? { capture: String(data(parts.get("capture")!)) }
|
|
1237
|
+
: {}),
|
|
1238
|
+
});
|
|
1239
|
+
else
|
|
1240
|
+
fail(
|
|
1241
|
+
move,
|
|
1242
|
+
`unknown transfer instruction ${op}`,
|
|
1243
|
+
"choose create, reserve, post, or void from internal_transfer",
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
} else if (key === "approval") {
|
|
1247
|
+
const approval = data(expr) as UdlAction["approval"];
|
|
1248
|
+
if (approval && (approval.input as unknown) === "material") {
|
|
1249
|
+
approval.input = {};
|
|
1250
|
+
materialApprovals.add(a);
|
|
1251
|
+
}
|
|
1252
|
+
a.approval = approval;
|
|
1253
|
+
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
1254
|
+
}
|
|
1255
|
+
if (automatic(a.actor)) delete a.publicAction;
|
|
1256
|
+
for (const requirement of a.requires ?? []) {
|
|
1257
|
+
if (
|
|
1258
|
+
requirement.kind !== "approval" ||
|
|
1259
|
+
!approvers.has(requirement.party)
|
|
1260
|
+
)
|
|
1261
|
+
continue;
|
|
1262
|
+
const action = requirement.action ?? name;
|
|
1263
|
+
const key = `${id}_${action}_decision`;
|
|
1264
|
+
const previous = implicitDecisions.get(key);
|
|
1265
|
+
if (previous && previous.party !== requirement.party)
|
|
1266
|
+
fail(
|
|
1267
|
+
{ span: origin },
|
|
1268
|
+
`action ${action} has multiple approval parties`,
|
|
1269
|
+
"use a separate decision action for each party",
|
|
1270
|
+
);
|
|
1271
|
+
implicitDecisions.set(key, {
|
|
1272
|
+
target: id,
|
|
1273
|
+
action,
|
|
1274
|
+
party: requirement.party,
|
|
1275
|
+
origin,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
inst.actions[name] = a;
|
|
1279
|
+
inst.actionOrder.push(name);
|
|
1280
|
+
}
|
|
1281
|
+
for (const key of ["invariants"] as const)
|
|
1282
|
+
if (body.has(key))
|
|
1283
|
+
(inst as unknown as Record<string, unknown>)[key] = data(
|
|
1284
|
+
body.get(key)!,
|
|
1285
|
+
);
|
|
1286
|
+
origins.push({
|
|
1287
|
+
path: `$.instruments[${document.instruments.length}]`,
|
|
1288
|
+
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
173
1289
|
});
|
|
1290
|
+
document.instruments.push(inst);
|
|
1291
|
+
for (const [key, definition] of records) {
|
|
1292
|
+
const child: InstrumentDecl = {
|
|
1293
|
+
kind: "instrument",
|
|
1294
|
+
name: key,
|
|
1295
|
+
parameters: [],
|
|
1296
|
+
body: asBlock(definition),
|
|
1297
|
+
span: origin,
|
|
1298
|
+
};
|
|
1299
|
+
addInstrument(
|
|
1300
|
+
child,
|
|
1301
|
+
`${id}_${key}`,
|
|
1302
|
+
emptyBlock,
|
|
1303
|
+
origin,
|
|
1304
|
+
new Map([
|
|
1305
|
+
...environment,
|
|
1306
|
+
["parent", { kind: "name", value: id, span: origin } as Expr],
|
|
1307
|
+
]),
|
|
1308
|
+
approvers,
|
|
1309
|
+
enums,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
for (const decl of program.decls) {
|
|
1314
|
+
if (decl.kind === "instrument") {
|
|
1315
|
+
if (decl.parameters.length)
|
|
1316
|
+
fail(
|
|
1317
|
+
decl,
|
|
1318
|
+
"program records cannot declare tunables",
|
|
1319
|
+
"put reusable instruments in a header",
|
|
1320
|
+
);
|
|
1321
|
+
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
1322
|
+
}
|
|
1323
|
+
if (decl.kind === "object") {
|
|
1324
|
+
const template = templates.get(decl.object);
|
|
1325
|
+
if (!template)
|
|
1326
|
+
fail(
|
|
1327
|
+
decl,
|
|
1328
|
+
`unknown object ${decl.object}`,
|
|
1329
|
+
`add use ${decl.object.split(".")[0]} and choose a declared object`,
|
|
1330
|
+
);
|
|
1331
|
+
addInstrument(template, decl.name, decl.body, decl.span);
|
|
1332
|
+
}
|
|
174
1333
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1334
|
+
const eliminatedStates = new Map<string, Set<string>>();
|
|
1335
|
+
// Remove branches excluded by immutable tunables before checking reference states.
|
|
1336
|
+
for (const inst of document.instruments) {
|
|
1337
|
+
for (const [key, action] of Object.entries(inst.actions)) {
|
|
1338
|
+
const constant = (value: import("@hyperscale0/udl").UdlValue) => {
|
|
1339
|
+
if ("literal" in value) return value.literal;
|
|
1340
|
+
const f = inst.fields.find((f) => `self.${f.name}` === value.field);
|
|
1341
|
+
return f && "value" in f ? f.value : undefined;
|
|
1342
|
+
};
|
|
1343
|
+
if (
|
|
1344
|
+
action.requires.some((r) => {
|
|
1345
|
+
if (r.kind !== "compare") return false;
|
|
1346
|
+
const left = constant(r.left),
|
|
1347
|
+
right = constant(r.right);
|
|
1348
|
+
if (left === undefined || right === undefined) return false;
|
|
1349
|
+
return r.operator === "=="
|
|
1350
|
+
? left !== right
|
|
1351
|
+
: r.operator === "!="
|
|
1352
|
+
? left === right
|
|
1353
|
+
: false;
|
|
1354
|
+
})
|
|
1355
|
+
) {
|
|
1356
|
+
delete inst.actions[key];
|
|
1357
|
+
delete inst.lifecycle.transitions[key];
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const reachable = new Set([inst.lifecycle.initial]);
|
|
1361
|
+
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
1362
|
+
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
1363
|
+
if (edge.from.some((state) => reachable.has(state)))
|
|
1364
|
+
reachable.add(edge.to);
|
|
1365
|
+
for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
|
|
1366
|
+
edge.from = edge.from.filter((state) => reachable.has(state));
|
|
1367
|
+
if (!edge.from.length) {
|
|
1368
|
+
delete inst.actions[key];
|
|
1369
|
+
delete inst.lifecycle.transitions[key];
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
eliminatedStates.set(
|
|
1373
|
+
inst.id,
|
|
1374
|
+
new Set(inst.lifecycle.states.filter((state) => !reachable.has(state))),
|
|
214
1375
|
);
|
|
1376
|
+
inst.lifecycle.states = inst.lifecycle.states.filter((state) =>
|
|
1377
|
+
reachable.has(state),
|
|
1378
|
+
);
|
|
1379
|
+
inst.actionOrder = inst.actionOrder.filter((key) => inst.actions[key]);
|
|
215
1380
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
1381
|
+
for (const inst of document.instruments)
|
|
1382
|
+
for (const action of Object.values(inst.actions)) {
|
|
1383
|
+
for (const requirement of action.requires) {
|
|
1384
|
+
if (requirement.kind !== "state") continue;
|
|
1385
|
+
const field = inst.fields.find(
|
|
1386
|
+
(f) => `self.${f.name}` === requirement.reference,
|
|
1387
|
+
);
|
|
1388
|
+
if (field?.type === "ref")
|
|
1389
|
+
requirement.states = requirement.states.filter(
|
|
1390
|
+
(state) =>
|
|
1391
|
+
!(
|
|
1392
|
+
typeof field.target === "string"
|
|
1393
|
+
? [field.target]
|
|
1394
|
+
: field.target
|
|
1395
|
+
).some((id) => eliminatedStates.get(id)?.has(state)),
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
// Approval tunables instantiate the same library record as an explicit decision.
|
|
1400
|
+
if (implicitDecisions.size) {
|
|
1401
|
+
const content = (
|
|
1402
|
+
options.standardLibrary ?? bundledStandardLibrary
|
|
1403
|
+
).source("approvals");
|
|
1404
|
+
const parsed = content ? parseProgram(content) : undefined;
|
|
1405
|
+
const template = parsed?.program.decls.find(
|
|
1406
|
+
(decl) => decl.kind === "instrument" && decl.name === "decision",
|
|
1407
|
+
);
|
|
1408
|
+
if (
|
|
1409
|
+
!template ||
|
|
1410
|
+
template.kind !== "instrument" ||
|
|
1411
|
+
parsed?.diagnostics.length
|
|
1412
|
+
)
|
|
1413
|
+
fail(
|
|
1414
|
+
program,
|
|
1415
|
+
"implicit decisions need the approvals header",
|
|
1416
|
+
"provide the standard approvals header",
|
|
1417
|
+
);
|
|
1418
|
+
for (const [id, decision] of implicitDecisions) {
|
|
1419
|
+
if (
|
|
1420
|
+
!document.instruments.find((inst) => inst.id === decision.target)
|
|
1421
|
+
?.actions[decision.action]
|
|
1422
|
+
)
|
|
1423
|
+
continue;
|
|
1424
|
+
const existing = document.instruments.some((inst) =>
|
|
1425
|
+
Object.values(inst.actions).some(
|
|
1426
|
+
(action) =>
|
|
1427
|
+
action.approval?.action === decision.action &&
|
|
1428
|
+
action.approval.party === decision.party &&
|
|
1429
|
+
inst.fields.some(
|
|
1430
|
+
(field) =>
|
|
1431
|
+
field.type === "ref" &&
|
|
1432
|
+
field.target === decision.target &&
|
|
1433
|
+
`self.${field.name}` === action.approval?.target,
|
|
1434
|
+
),
|
|
1435
|
+
),
|
|
1436
|
+
);
|
|
1437
|
+
if (existing) continue;
|
|
1438
|
+
if (document.instruments.some((inst) => inst.id === id))
|
|
1439
|
+
fail(
|
|
1440
|
+
{ span: decision.origin },
|
|
1441
|
+
`implicit decision name ${id} is already used`,
|
|
1442
|
+
"rename the conflicting object",
|
|
1443
|
+
);
|
|
1444
|
+
addInstrument(
|
|
1445
|
+
template,
|
|
1446
|
+
id,
|
|
1447
|
+
{
|
|
1448
|
+
kind: "block",
|
|
1449
|
+
span: decision.origin,
|
|
1450
|
+
entries: Object.entries({
|
|
1451
|
+
for: decision.target,
|
|
1452
|
+
approved_by: decision.party,
|
|
1453
|
+
action: decision.action,
|
|
1454
|
+
}).map(([key, value]) => ({
|
|
1455
|
+
key,
|
|
1456
|
+
value: {
|
|
1457
|
+
kind: key === "action" ? "text" : "name",
|
|
1458
|
+
value,
|
|
1459
|
+
span: decision.origin,
|
|
1460
|
+
},
|
|
1461
|
+
span: decision.origin,
|
|
1462
|
+
})),
|
|
1463
|
+
},
|
|
1464
|
+
decision.origin,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
227
1467
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
1468
|
+
// Material approval fields are inferred from the selected action's typed input.
|
|
1469
|
+
for (const inst of document.instruments)
|
|
1470
|
+
for (const action of Object.values(inst.actions)) {
|
|
1471
|
+
if (!materialApprovals.has(action) || !action.approval) continue;
|
|
1472
|
+
const targetField = inst.fields.find(
|
|
1473
|
+
(f) => `self.${f.name}` === action.approval!.target,
|
|
1474
|
+
);
|
|
1475
|
+
if (targetField?.type !== "ref") continue;
|
|
1476
|
+
const target = document.instruments.find(
|
|
1477
|
+
(i) => i.id === targetField.target,
|
|
1478
|
+
)?.actions[action.approval.action];
|
|
1479
|
+
if (!target) continue;
|
|
1480
|
+
for (const field of target.input) {
|
|
1481
|
+
const name = `material_${field.name}`;
|
|
1482
|
+
if (!inst.fields.some((f) => f.name === name))
|
|
1483
|
+
inst.fields.push({ ...field, name });
|
|
1484
|
+
action.approval.input[field.name] = { field: `self.${name}` };
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const changed = new Set<string>();
|
|
1488
|
+
for (const decl of program.decls)
|
|
1489
|
+
if (decl.kind === "hide" || decl.kind === "expose") {
|
|
1490
|
+
const parts = decl.target.split(".");
|
|
1491
|
+
const key = parts.pop();
|
|
1492
|
+
const id = parts.join("_");
|
|
1493
|
+
const action = document.instruments.find((i) => i.id === id)?.actions[
|
|
1494
|
+
key ?? ""
|
|
1495
|
+
];
|
|
1496
|
+
if (!action || changed.has(decl.target))
|
|
1497
|
+
fail(
|
|
1498
|
+
decl,
|
|
1499
|
+
`unknown or repeated action ${decl.target}`,
|
|
1500
|
+
"name one declared object.action once",
|
|
1501
|
+
);
|
|
1502
|
+
changed.add(decl.target);
|
|
1503
|
+
if (decl.kind === "hide") delete action!.publicAction;
|
|
1504
|
+
else if (automatic(action!.actor))
|
|
1505
|
+
fail(
|
|
1506
|
+
decl,
|
|
1507
|
+
`${decl.target} runs on the clock or its parent, not a caller`,
|
|
1508
|
+
"expose only caller actions",
|
|
1509
|
+
);
|
|
1510
|
+
else action!.publicAction = decl.name!;
|
|
1511
|
+
}
|
|
1512
|
+
const usedParties = new Set<string>();
|
|
1513
|
+
const collectParties = (value: unknown): void => {
|
|
1514
|
+
if (typeof value === "string") {
|
|
1515
|
+
usedParties.add(value);
|
|
1516
|
+
if (value.startsWith("party.")) usedParties.add(value.split(".")[1]!);
|
|
1517
|
+
} else if (Array.isArray(value)) value.forEach(collectParties);
|
|
1518
|
+
else if (value && typeof value === "object")
|
|
1519
|
+
Object.values(value).forEach(collectParties);
|
|
1520
|
+
};
|
|
1521
|
+
collectParties(document.instruments);
|
|
1522
|
+
for (const party of ["programFines", "programCosts"])
|
|
1523
|
+
if (!usedParties.has(party) && !names.has(party))
|
|
1524
|
+
delete document.parties[party];
|
|
1525
|
+
const validated = validateUdl(document);
|
|
1526
|
+
if (!validated.ok)
|
|
1527
|
+
return {
|
|
1528
|
+
verdict: "invalid",
|
|
1529
|
+
diagnostics: validated.issues.map((i) => {
|
|
1530
|
+
const origin = [...origins]
|
|
1531
|
+
.reverse()
|
|
1532
|
+
.find((o) => i.path.startsWith(o.path));
|
|
1533
|
+
return diagnostic(
|
|
1534
|
+
{
|
|
1535
|
+
code: "HSX1601",
|
|
1536
|
+
message: `${i.path}: ${i.message}`,
|
|
1537
|
+
fix: i.fix,
|
|
1538
|
+
span: origin?.span ?? program.span,
|
|
1539
|
+
},
|
|
1540
|
+
"lower",
|
|
1541
|
+
);
|
|
1542
|
+
}),
|
|
1543
|
+
};
|
|
232
1544
|
return {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
start: byteOffsets[entry.span.start] ?? 0,
|
|
1545
|
+
verdict: "valid",
|
|
1546
|
+
diagnostics: [],
|
|
1547
|
+
artifacts: {
|
|
1548
|
+
document: validated.value,
|
|
1549
|
+
costManifest: buildUdlCostManifest(validated.value),
|
|
1550
|
+
originMap: origins,
|
|
240
1551
|
},
|
|
241
1552
|
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
costManifest: cost.manifest,
|
|
250
|
-
originMap: lowered.value.originMap.map(compileOrigin),
|
|
251
|
-
},
|
|
252
|
-
diagnostics,
|
|
253
|
-
verdict: diagnostics.length > 0 ? "warning" : "valid",
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function utf8OffsetIndex(source: string): Uint32Array {
|
|
258
|
-
const offsets = new Uint32Array(source.length + 1);
|
|
259
|
-
let bytes = 0;
|
|
260
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
261
|
-
offsets[index] = bytes;
|
|
262
|
-
const codePoint = source.codePointAt(index) as number;
|
|
263
|
-
if (codePoint > 0xffff) {
|
|
264
|
-
offsets[index + 1] = bytes;
|
|
265
|
-
bytes += 4;
|
|
266
|
-
index += 1;
|
|
267
|
-
} else if (codePoint > 0x7ff) bytes += 3;
|
|
268
|
-
else if (codePoint > 0x7f) bytes += 2;
|
|
269
|
-
else bytes += 1;
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (error instanceof CompileFailure)
|
|
1555
|
+
return {
|
|
1556
|
+
verdict: "invalid",
|
|
1557
|
+
diagnostics: [diagnostic(error.diagnostic, "check")],
|
|
1558
|
+
};
|
|
1559
|
+
throw error;
|
|
270
1560
|
}
|
|
271
|
-
offsets[source.length] = bytes;
|
|
272
|
-
return offsets;
|
|
273
1561
|
}
|