@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/cost.ts
CHANGED
|
@@ -1,675 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
udlEffectKinds,
|
|
3
|
-
resolveUdlActionPlans,
|
|
4
|
-
type UdlDocument,
|
|
5
|
-
type UdlEffectKind,
|
|
6
|
-
} from "@hyperscale0/udl";
|
|
7
|
-
import type {
|
|
8
|
-
GeneralDiagnostic,
|
|
9
|
-
JsonValue,
|
|
10
|
-
TypedField,
|
|
11
|
-
TypedProgram,
|
|
12
|
-
} from "./ir.ts";
|
|
13
|
-
|
|
14
|
-
export type HsxEffectKind = UdlEffectKind;
|
|
15
|
-
|
|
16
|
-
const CURRENCY = /^[A-Z]{3}$/;
|
|
17
|
-
|
|
18
|
-
export interface UdlCostTable {
|
|
19
|
-
readonly version: string;
|
|
20
|
-
/** Digest of the effective prices and fixed anchors after overrides. */
|
|
21
|
-
readonly effectiveTableDigest: string;
|
|
22
|
-
readonly currency: string;
|
|
23
|
-
/** Billable runtime meters the platform's operation contract can emit. */
|
|
24
|
-
readonly declaredMeters: readonly string[];
|
|
25
|
-
readonly rows: readonly {
|
|
26
|
-
readonly signature: string;
|
|
27
|
-
readonly perEventMinor: string;
|
|
28
|
-
readonly bps: number;
|
|
29
|
-
readonly payer: "product" | "end_customer";
|
|
30
|
-
/** Collection policy. Runtime resolves movement rows to execution or invoice. */
|
|
31
|
-
readonly settlement: "per_event" | "invoice";
|
|
32
|
-
readonly meter: string;
|
|
33
|
-
readonly volumeMeter?: string;
|
|
34
|
-
}[];
|
|
35
|
-
readonly fixed: {
|
|
36
|
-
readonly weights: Readonly<Record<string, number>>;
|
|
37
|
-
readonly monthlyBaseMinor: string;
|
|
38
|
-
readonly monthlyPerPointMinor: string;
|
|
39
|
-
readonly activationBaseMinor: string;
|
|
40
|
-
readonly activationPerPointMinor: string;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
1
|
+
import type { UdlDocument } from "@hyperscale0/udl";
|
|
44
2
|
export interface UdlCostManifest {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
readonly actions: readonly {
|
|
60
|
-
readonly instrument: string;
|
|
61
|
-
readonly action: string;
|
|
62
|
-
readonly components: readonly {
|
|
63
|
-
readonly signature: string;
|
|
64
|
-
readonly count: number;
|
|
65
|
-
readonly perEventMinor: string;
|
|
66
|
-
readonly bps: number;
|
|
67
|
-
readonly payer: "product" | "end_customer";
|
|
68
|
-
readonly settlement: "per_event" | "invoice";
|
|
69
|
-
readonly meter: string;
|
|
70
|
-
readonly volumeMeter?: string;
|
|
71
|
-
}[];
|
|
72
|
-
readonly perEventMinor: string;
|
|
73
|
-
readonly endCustomerPerEventMinor: string;
|
|
74
|
-
}[];
|
|
75
|
-
/** Frozen list prices for every declared runtime meter. */
|
|
76
|
-
readonly meterPrices: readonly UdlCostTable["rows"][number][];
|
|
77
|
-
readonly monthlyEstimate: {
|
|
78
|
-
readonly expression: string;
|
|
79
|
-
readonly variables: readonly string[];
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** One cost table, or one per billing currency the host prices. */
|
|
84
|
-
export type UdlCostTables = UdlCostTable | readonly UdlCostTable[];
|
|
85
|
-
|
|
86
|
-
export type CostManifestResult =
|
|
87
|
-
| { readonly diagnostics: readonly GeneralDiagnostic[]; readonly ok: false }
|
|
88
|
-
| { readonly manifest: UdlCostManifest; readonly ok: true };
|
|
89
|
-
|
|
90
|
-
export function buildCostManifest(
|
|
91
|
-
program: TypedProgram,
|
|
92
|
-
tables: UdlCostTables | undefined,
|
|
93
|
-
): CostManifestResult {
|
|
94
|
-
const priced = tables === undefined ? [] : [tables].flat();
|
|
95
|
-
if (priced.length === 0) {
|
|
96
|
-
return {
|
|
97
|
-
diagnostics: [
|
|
98
|
-
{
|
|
99
|
-
code: "HSX1301",
|
|
100
|
-
fix: "supply the versioned UDL cost table to the compiler",
|
|
101
|
-
message:
|
|
102
|
-
"the compiler cannot price this program without a cost table",
|
|
103
|
-
severity: "error",
|
|
104
|
-
span: program.origin,
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
ok: false,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const malformed = priced.find(
|
|
112
|
-
(candidate) => !CURRENCY.test(candidate.currency),
|
|
113
|
-
);
|
|
114
|
-
if (malformed) {
|
|
115
|
-
return {
|
|
116
|
-
diagnostics: [
|
|
117
|
-
{
|
|
118
|
-
code: "HSX1303",
|
|
119
|
-
fix: "name one three-letter uppercase billing currency on the cost table",
|
|
120
|
-
message: `cost table ${malformed.version} has invalid billing currency ${malformed.currency}`,
|
|
121
|
-
severity: "error",
|
|
122
|
-
span: program.origin,
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
ok: false,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const diagnostics: GeneralDiagnostic[] = [];
|
|
130
|
-
const table = ledgerCostTable(program, priced, diagnostics);
|
|
131
|
-
if (!table) return { diagnostics, ok: false };
|
|
132
|
-
const rowsBySignature = indexCostRows(table);
|
|
133
|
-
const actions: UdlCostManifest["actions"][number][] = [];
|
|
134
|
-
for (const instrument of program.instruments) {
|
|
135
|
-
for (const action of instrument.actions) {
|
|
136
|
-
actions.push(
|
|
137
|
-
priceAction(
|
|
138
|
-
instrument.id,
|
|
139
|
-
action.name,
|
|
140
|
-
effectCounts(action.effects),
|
|
141
|
-
rowsBySignature,
|
|
142
|
-
(issue) => {
|
|
143
|
-
if (issue.kind === "missing_price") {
|
|
144
|
-
diagnostics.push({
|
|
145
|
-
code: "HSX1301",
|
|
146
|
-
fix: `add ${issue.signature} to cost table ${table.version}`,
|
|
147
|
-
message: `${instrument.id}.${action.name} has unpriced effect signature ${issue.signature}`,
|
|
148
|
-
severity: "error",
|
|
149
|
-
span: action.origin,
|
|
150
|
-
});
|
|
151
|
-
} else {
|
|
152
|
-
diagnostics.push({
|
|
153
|
-
code: "HSX1302",
|
|
154
|
-
fix: `store ${issue.signature} prices as nonnegative integers and name a volume meter for bps`,
|
|
155
|
-
message: `cost table ${table.version} has an invalid ${issue.signature} price`,
|
|
156
|
-
severity: "error",
|
|
157
|
-
span: action.origin,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const fixed = fixedCost(
|
|
166
|
-
typedStructuralCounts(program),
|
|
167
|
-
table,
|
|
168
|
-
program.origin,
|
|
169
|
-
diagnostics,
|
|
170
|
-
);
|
|
171
|
-
if (diagnostics.length > 0) return { diagnostics, ok: false };
|
|
172
|
-
return { manifest: manifest(table, fixed, actions), ok: true };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The table priced in the program's ledger currency: the one currency its
|
|
177
|
-
* money fields move. A program without money fields prices in the host's
|
|
178
|
-
* first table.
|
|
179
|
-
*/
|
|
180
|
-
function ledgerCostTable(
|
|
181
|
-
program: TypedProgram,
|
|
182
|
-
tables: readonly UdlCostTable[],
|
|
183
|
-
diagnostics: GeneralDiagnostic[],
|
|
184
|
-
): UdlCostTable | undefined {
|
|
185
|
-
const currencies = new Map<string, TypedField["origin"]>();
|
|
186
|
-
for (const instrument of program.instruments) {
|
|
187
|
-
for (const field of instrument.fields) {
|
|
188
|
-
// An unbound currency parameter (money<C>) is decided per instance,
|
|
189
|
-
// not by the program, so it names no ledger currency.
|
|
190
|
-
const currency = field.type.currency;
|
|
191
|
-
if (field.type.kind !== "money" || !currency || !CURRENCY.test(currency))
|
|
192
|
-
continue;
|
|
193
|
-
if (!currencies.has(currency)) currencies.set(currency, field.origin);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const [first, second] = [...currencies];
|
|
197
|
-
if (second) {
|
|
198
|
-
diagnostics.push({
|
|
199
|
-
code: "HSX1305",
|
|
200
|
-
fix: `move all money in ${first![0]}; a program bills in one ledger currency`,
|
|
201
|
-
message: `program moves money in ${[...currencies.keys()].join(" and ")}; a program bills in one ledger currency`,
|
|
202
|
-
severity: "error",
|
|
203
|
-
span: second[1],
|
|
204
|
-
});
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
const ledger = first?.[0] ?? tables[0]!.currency;
|
|
208
|
-
const table = tables.find((candidate) => candidate.currency === ledger);
|
|
209
|
-
if (table) return table;
|
|
210
|
-
const priced = tables.map((candidate) => candidate.currency).join(", ");
|
|
211
|
-
diagnostics.push({
|
|
212
|
-
code: "HSX1304",
|
|
213
|
-
fix: `move money in a priced currency: ${priced}`,
|
|
214
|
-
message: `program moves money in ${ledger}, which no cost table prices (priced: ${priced})`,
|
|
215
|
-
severity: "error",
|
|
216
|
-
span: first?.[1] ?? program.origin,
|
|
217
|
-
});
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function computeUdlFixedCost(
|
|
222
|
-
document: UdlDocument,
|
|
223
|
-
table: UdlCostTable,
|
|
224
|
-
): UdlCostManifest["fixed"] {
|
|
225
|
-
const diagnostics: GeneralDiagnostic[] = [];
|
|
226
|
-
const fixed = fixedCost(
|
|
227
|
-
udlStructuralCounts(document),
|
|
228
|
-
table,
|
|
229
|
-
{ start: 0, end: 0 },
|
|
230
|
-
diagnostics,
|
|
231
|
-
);
|
|
232
|
-
if (diagnostics.length > 0) throw new Error(diagnostics[0]?.message);
|
|
233
|
-
return fixed;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function buildUdlCostManifest(
|
|
237
|
-
document: UdlDocument,
|
|
238
|
-
table: UdlCostTable,
|
|
239
|
-
): UdlCostManifest {
|
|
240
|
-
if (!CURRENCY.test(table.currency)) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
`cost table ${table.version} has invalid billing currency ${table.currency}`,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
const rowsBySignature = indexCostRows(table);
|
|
246
|
-
const actions: UdlCostManifest["actions"][number][] = [];
|
|
247
|
-
for (const instrument of document.instruments) {
|
|
248
|
-
const resolved = resolveUdlActionPlans(instrument);
|
|
249
|
-
if (resolved.issues.length > 0)
|
|
250
|
-
throw new Error(resolved.issues[0]!.message);
|
|
251
|
-
for (const actionName of instrument.actionOrder) {
|
|
252
|
-
const action = instrument.actions[actionName]!;
|
|
253
|
-
const plan = resolved.plans.find(
|
|
254
|
-
(candidate) => candidate.action === actionName,
|
|
255
|
-
);
|
|
256
|
-
const effects = (
|
|
257
|
-
action.calls && plan ? plan.effects : (action.effects ?? {})
|
|
258
|
-
) as Readonly<
|
|
259
|
-
Partial<
|
|
260
|
-
Record<
|
|
261
|
-
HsxEffectKind,
|
|
262
|
-
readonly { readonly signature: string; readonly source: string }[]
|
|
263
|
-
>
|
|
264
|
-
>
|
|
265
|
-
>;
|
|
266
|
-
actions.push(
|
|
267
|
-
priceAction(
|
|
268
|
-
instrument.id,
|
|
269
|
-
actionName,
|
|
270
|
-
effectCounts(effects),
|
|
271
|
-
rowsBySignature,
|
|
272
|
-
(issue) => {
|
|
273
|
-
if (issue.kind === "missing_price") {
|
|
274
|
-
throw new Error(
|
|
275
|
-
`${instrument.id}.${actionName} has unpriced effect signature ${issue.signature}`,
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
throw new Error(
|
|
279
|
-
`cost table ${table.version} has an invalid ${issue.signature} price`,
|
|
280
|
-
);
|
|
281
|
-
},
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return manifest(table, computeUdlFixedCost(document, table), actions);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function evaluateUdlCostManifest(
|
|
290
|
-
costManifest: UdlCostManifest,
|
|
291
|
-
readings: Readonly<Record<string, string>>,
|
|
292
|
-
): string {
|
|
293
|
-
let total = BigInt(costManifest.fixed.monthlyMinor);
|
|
294
|
-
const priced = new Set<string>();
|
|
295
|
-
for (const price of costManifest.meterPrices) {
|
|
296
|
-
if (price.payer !== "product") continue;
|
|
297
|
-
const key = pricingKey(price);
|
|
298
|
-
if (priced.has(key)) continue;
|
|
299
|
-
priced.add(key);
|
|
300
|
-
const count = scaledDecimal(readings[price.meter] ?? "0");
|
|
301
|
-
total += (BigInt(price.perEventMinor) * count) / 1_000_000n;
|
|
302
|
-
if (price.bps > 0 && price.volumeMeter) {
|
|
303
|
-
const volume = scaledDecimal(readings[price.volumeMeter] ?? "0");
|
|
304
|
-
total += (volume * BigInt(price.bps)) / 10_000n / 1_000_000n;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return total.toString();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
interface ActionPricingIssue {
|
|
311
|
-
readonly kind: "invalid_price" | "missing_price";
|
|
312
|
-
readonly signature: string;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function indexCostRows(
|
|
316
|
-
table: UdlCostTable,
|
|
317
|
-
): ReadonlyMap<string, readonly UdlCostTable["rows"][number][]> {
|
|
318
|
-
const rowsBySignature = new Map<string, UdlCostTable["rows"][number][]>();
|
|
319
|
-
for (const row of table.rows) {
|
|
320
|
-
const existing = rowsBySignature.get(row.signature);
|
|
321
|
-
if (existing) existing.push(row);
|
|
322
|
-
else rowsBySignature.set(row.signature, [row]);
|
|
323
|
-
}
|
|
324
|
-
return rowsBySignature;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function priceAction(
|
|
328
|
-
instrument: string,
|
|
329
|
-
action: string,
|
|
330
|
-
counts: Iterable<readonly [string, number]>,
|
|
331
|
-
rowsBySignature: ReadonlyMap<string, readonly UdlCostTable["rows"][number][]>,
|
|
332
|
-
onIssue: (issue: ActionPricingIssue) => void,
|
|
333
|
-
): UdlCostManifest["actions"][number] {
|
|
334
|
-
const components: UdlCostManifest["actions"][number]["components"][number][] =
|
|
335
|
-
[];
|
|
336
|
-
let productPerEvent = 0n;
|
|
337
|
-
let endCustomerPerEvent = 0n;
|
|
338
|
-
for (const [signature, count] of counts) {
|
|
339
|
-
const rows = rowsBySignature.get(signature);
|
|
340
|
-
if (!rows || rows.length === 0) {
|
|
341
|
-
onIssue({ kind: "missing_price", signature });
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
for (const row of rows) {
|
|
345
|
-
if (!validPrice(row)) {
|
|
346
|
-
onIssue({ kind: "invalid_price", signature });
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
|
|
350
|
-
if (row.payer === "product") productPerEvent += eventTotal;
|
|
351
|
-
else endCustomerPerEvent += eventTotal;
|
|
352
|
-
components.push({
|
|
353
|
-
bps: row.bps,
|
|
354
|
-
count,
|
|
355
|
-
meter: row.meter,
|
|
356
|
-
payer: row.payer,
|
|
357
|
-
perEventMinor: row.perEventMinor,
|
|
358
|
-
settlement: row.settlement,
|
|
359
|
-
signature,
|
|
360
|
-
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
action,
|
|
366
|
-
components,
|
|
367
|
-
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
368
|
-
instrument,
|
|
369
|
-
perEventMinor: productPerEvent.toString(),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function manifest(
|
|
374
|
-
table: UdlCostTable,
|
|
375
|
-
fixed: UdlCostManifest["fixed"],
|
|
376
|
-
actions: UdlCostManifest["actions"],
|
|
377
|
-
): UdlCostManifest {
|
|
378
|
-
const declaredMeters = [...new Set(table.declaredMeters)].sort();
|
|
379
|
-
const declaredMeterSet = new Set(declaredMeters);
|
|
380
|
-
const meterPrices = table.rows.filter(
|
|
381
|
-
(row) =>
|
|
382
|
-
(row.perEventMinor !== "0" || row.bps > 0) &&
|
|
383
|
-
(declaredMeterSet.has(row.meter) ||
|
|
384
|
-
(row.volumeMeter !== undefined &&
|
|
385
|
-
declaredMeterSet.has(row.volumeMeter))),
|
|
386
|
-
);
|
|
387
|
-
const variables = new Set<string>();
|
|
388
|
-
const terms = [fixed.monthlyMinor];
|
|
389
|
-
const priced = new Set<string>();
|
|
390
|
-
for (const price of meterPrices) {
|
|
391
|
-
if (price.payer !== "product") continue;
|
|
392
|
-
const key = pricingKey(price);
|
|
393
|
-
if (priced.has(key)) continue;
|
|
394
|
-
priced.add(key);
|
|
395
|
-
variables.add(price.meter);
|
|
396
|
-
if (price.perEventMinor !== "0")
|
|
397
|
-
terms.push(`${price.perEventMinor} * ${price.meter}`);
|
|
398
|
-
if (price.bps > 0 && price.volumeMeter) {
|
|
399
|
-
variables.add(price.volumeMeter);
|
|
400
|
-
terms.push(`${price.bps} / 10000 * ${price.volumeMeter}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
actions,
|
|
405
|
-
costTableVersion: table.version,
|
|
406
|
-
currency: table.currency,
|
|
407
|
-
declaredMeters,
|
|
408
|
-
effectiveTableDigest: table.effectiveTableDigest,
|
|
409
|
-
fixed,
|
|
410
|
-
meterPrices,
|
|
411
|
-
monthlyEstimate: {
|
|
412
|
-
expression: terms.join(" + "),
|
|
413
|
-
variables: [...variables].sort(),
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function pricingKey(
|
|
419
|
-
component: Pick<
|
|
420
|
-
UdlCostTable["rows"][number],
|
|
421
|
-
| "bps"
|
|
422
|
-
| "meter"
|
|
423
|
-
| "payer"
|
|
424
|
-
| "perEventMinor"
|
|
425
|
-
| "settlement"
|
|
426
|
-
| "signature"
|
|
427
|
-
| "volumeMeter"
|
|
428
|
-
>,
|
|
429
|
-
): string {
|
|
430
|
-
return [
|
|
431
|
-
component.signature,
|
|
432
|
-
component.meter,
|
|
433
|
-
component.volumeMeter ?? "",
|
|
434
|
-
component.perEventMinor,
|
|
435
|
-
String(component.bps),
|
|
436
|
-
component.payer,
|
|
437
|
-
component.settlement,
|
|
438
|
-
].join("\u0000");
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function effectCounts(
|
|
442
|
-
effects: Readonly<
|
|
443
|
-
Partial<
|
|
444
|
-
Record<
|
|
445
|
-
HsxEffectKind,
|
|
446
|
-
readonly { readonly signature: string; readonly source: string }[]
|
|
447
|
-
>
|
|
448
|
-
>
|
|
449
|
-
>,
|
|
450
|
-
): readonly [string, number][] {
|
|
451
|
-
const counts = new Map<string, number>();
|
|
452
|
-
for (const kind of udlEffectKinds) {
|
|
453
|
-
for (const effect of effects[kind] ?? [])
|
|
454
|
-
counts.set(effect.signature, (counts.get(effect.signature) ?? 0) + 1);
|
|
455
|
-
}
|
|
456
|
-
return [...counts.entries()].sort(([left], [right]) =>
|
|
457
|
-
left.localeCompare(right),
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function fixedCost(
|
|
462
|
-
counts: Readonly<Record<string, number>>,
|
|
463
|
-
table: UdlCostTable,
|
|
464
|
-
span: { readonly start: number; readonly end: number },
|
|
465
|
-
diagnostics: GeneralDiagnostic[],
|
|
466
|
-
): UdlCostManifest["fixed"] {
|
|
467
|
-
const components: UdlCostManifest["fixed"]["components"][number][] = [];
|
|
468
|
-
let complexityScore = 0;
|
|
469
|
-
for (const weight of Object.keys(table.fixed.weights)) {
|
|
470
|
-
if (Object.hasOwn(counts, weight)) continue;
|
|
471
|
-
diagnostics.push({
|
|
472
|
-
code: "HSX1302",
|
|
473
|
-
fix: `remove the unknown ${weight} weight from cost table ${table.version}`,
|
|
474
|
-
message: `cost table ${table.version} has an unknown ${weight} complexity weight`,
|
|
475
|
-
severity: "error",
|
|
476
|
-
span,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
for (const [weight, count] of Object.entries(counts)) {
|
|
480
|
-
if (count === 0) continue;
|
|
481
|
-
const value = table.fixed.weights[weight];
|
|
482
|
-
if (value === undefined || !Number.isSafeInteger(value) || value < 0) {
|
|
483
|
-
diagnostics.push({
|
|
484
|
-
code: "HSX1302",
|
|
485
|
-
fix: `add a nonnegative integer ${weight} weight to cost table ${table.version}`,
|
|
486
|
-
message: `cost table ${table.version} has an invalid ${weight} complexity weight`,
|
|
487
|
-
severity: "error",
|
|
488
|
-
span,
|
|
489
|
-
});
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
complexityScore += value * count;
|
|
493
|
-
components.push({ count, weight });
|
|
494
|
-
}
|
|
495
|
-
for (const [field, value] of Object.entries(table.fixed).filter(
|
|
496
|
-
([key]) => key !== "weights",
|
|
497
|
-
)) {
|
|
498
|
-
if (typeof value === "string" && nonnegativeInteger(value)) continue;
|
|
499
|
-
diagnostics.push({
|
|
500
|
-
code: "HSX1302",
|
|
501
|
-
fix: `store fixed.${field} as a nonnegative decimal integer`,
|
|
502
|
-
message: `cost table ${table.version} has an invalid fixed ${field} price`,
|
|
503
|
-
severity: "error",
|
|
504
|
-
span,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
return {
|
|
508
|
-
activationMinor: (
|
|
509
|
-
integerOrZero(table.fixed.activationBaseMinor) +
|
|
510
|
-
integerOrZero(table.fixed.activationPerPointMinor) *
|
|
511
|
-
BigInt(complexityScore)
|
|
512
|
-
).toString(),
|
|
513
|
-
complexityScore,
|
|
514
|
-
components,
|
|
515
|
-
monthlyMinor: (
|
|
516
|
-
integerOrZero(table.fixed.monthlyBaseMinor) +
|
|
517
|
-
integerOrZero(table.fixed.monthlyPerPointMinor) * BigInt(complexityScore)
|
|
518
|
-
).toString(),
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function typedStructuralCounts(
|
|
523
|
-
program: TypedProgram,
|
|
524
|
-
): Readonly<Record<string, number>> {
|
|
525
|
-
const subjectKinds = new Set<string>();
|
|
526
|
-
const decisions = new Set<string>();
|
|
527
|
-
let flow = 0,
|
|
528
|
-
move = 0,
|
|
529
|
-
unwind = 0,
|
|
530
|
-
aggregate = 0,
|
|
531
|
-
gate = 0,
|
|
532
|
-
timer = 0;
|
|
533
|
-
for (const instrument of program.instruments) {
|
|
534
|
-
for (const kind of arrayValue(objectValue(instrument.slots.subject)?.kinds))
|
|
535
|
-
if (typeof kind === "string") subjectKinds.add(kind);
|
|
536
|
-
if (instrument.actions.some((action) => action.slots.quote !== undefined))
|
|
537
|
-
unwind += 1;
|
|
538
|
-
aggregate += arrayValue(instrument.slots.aggregateInvariants).length;
|
|
539
|
-
for (const action of instrument.actions) {
|
|
540
|
-
flow += 1;
|
|
541
|
-
const plan = instrument.actionPlans?.find(
|
|
542
|
-
(plan) => plan.action === action.name,
|
|
543
|
-
);
|
|
544
|
-
move +=
|
|
545
|
-
action.slots.calls && plan
|
|
546
|
-
? plan.leaves.filter((leaf) =>
|
|
547
|
-
leaf.step.operation.startsWith("internal_transfer."),
|
|
548
|
-
).length
|
|
549
|
-
: arrayValue(action.slots.moves).length;
|
|
550
|
-
gate += arrayValue(action.slots.requiresRefs).length;
|
|
551
|
-
if (action.slots.deadline !== undefined) timer += 1;
|
|
552
|
-
if (action.slots.due !== undefined) timer += 1;
|
|
553
|
-
const capability = objectValue(action.slots.decision)?.capability;
|
|
554
|
-
if (typeof capability === "string") decisions.add(capability);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return countRecord(
|
|
558
|
-
program.instruments.length,
|
|
559
|
-
subjectKinds.size,
|
|
560
|
-
flow,
|
|
561
|
-
move,
|
|
562
|
-
decisions.size,
|
|
563
|
-
unwind,
|
|
564
|
-
aggregate,
|
|
565
|
-
gate,
|
|
566
|
-
timer,
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function udlStructuralCounts(
|
|
571
|
-
document: UdlDocument,
|
|
572
|
-
): Readonly<Record<string, number>> {
|
|
573
|
-
const subjectKinds = new Set<string>();
|
|
574
|
-
const decisions = new Set<string>();
|
|
575
|
-
let flow = 0,
|
|
576
|
-
move = 0,
|
|
577
|
-
unwind = 0,
|
|
578
|
-
aggregate = 0,
|
|
579
|
-
gate = 0,
|
|
580
|
-
timer = 0;
|
|
581
|
-
for (const instrument of document.instruments) {
|
|
582
|
-
for (const kind of instrument.subject?.kinds ?? []) subjectKinds.add(kind);
|
|
583
|
-
if (Object.values(instrument.actions).some((action) => action.quote))
|
|
584
|
-
unwind += 1;
|
|
585
|
-
aggregate += instrument.aggregateInvariants?.length ?? 0;
|
|
586
|
-
const resolved = resolveUdlActionPlans(instrument);
|
|
587
|
-
if (resolved.issues.length > 0)
|
|
588
|
-
throw new Error(resolved.issues[0]!.message);
|
|
3
|
+
actions: Record<
|
|
4
|
+
string,
|
|
5
|
+
{
|
|
6
|
+
transfers: number;
|
|
7
|
+
accounts: number;
|
|
8
|
+
invocations: number;
|
|
9
|
+
}
|
|
10
|
+
>;
|
|
11
|
+
}
|
|
12
|
+
/** Counts sealed instructions after lowering. Prices belong to the executor's tariff. */
|
|
13
|
+
export function buildUdlCostManifest(document: UdlDocument): UdlCostManifest {
|
|
14
|
+
const actions: UdlCostManifest["actions"] = {};
|
|
15
|
+
for (const instrument of document.instruments)
|
|
589
16
|
for (const [name, action] of Object.entries(instrument.actions)) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
17
|
+
actions[`${instrument.id}.${name}`] = {
|
|
18
|
+
transfers: action.moves.length,
|
|
19
|
+
accounts:
|
|
20
|
+
name === "create"
|
|
21
|
+
? instrument.fields.filter(
|
|
22
|
+
(f) => f.type === "account" && f.owner === "self",
|
|
23
|
+
).length
|
|
24
|
+
: 0,
|
|
25
|
+
invocations: (action.invoke ?? []).reduce(
|
|
26
|
+
(sum, call) => sum + ("selection" in call ? call.selection.limit : 1),
|
|
27
|
+
0,
|
|
28
|
+
),
|
|
29
|
+
};
|
|
602
30
|
}
|
|
603
|
-
}
|
|
604
|
-
return countRecord(
|
|
605
|
-
document.instruments.length,
|
|
606
|
-
subjectKinds.size,
|
|
607
|
-
flow,
|
|
608
|
-
move,
|
|
609
|
-
decisions.size,
|
|
610
|
-
unwind,
|
|
611
|
-
aggregate,
|
|
612
|
-
gate,
|
|
613
|
-
timer,
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function countRecord(
|
|
618
|
-
instrument: number,
|
|
619
|
-
subjectKind: number,
|
|
620
|
-
flow: number,
|
|
621
|
-
move: number,
|
|
622
|
-
providerDecision: number,
|
|
623
|
-
unwind: number,
|
|
624
|
-
aggregateInvariant: number,
|
|
625
|
-
gate: number,
|
|
626
|
-
timer: number,
|
|
627
|
-
): Readonly<Record<string, number>> {
|
|
628
|
-
return {
|
|
629
|
-
instrument,
|
|
630
|
-
subject_kind: subjectKind,
|
|
631
|
-
flow,
|
|
632
|
-
move,
|
|
633
|
-
provider_decision: providerDecision,
|
|
634
|
-
unwind,
|
|
635
|
-
aggregate_invariant: aggregateInvariant,
|
|
636
|
-
gate,
|
|
637
|
-
timer,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function validPrice(row: UdlCostTable["rows"][number]): boolean {
|
|
642
|
-
return (
|
|
643
|
-
nonnegativeInteger(row.perEventMinor) &&
|
|
644
|
-
Number.isSafeInteger(row.bps) &&
|
|
645
|
-
row.bps >= 0 &&
|
|
646
|
-
(row.settlement === "per_event" || row.settlement === "invoice") &&
|
|
647
|
-
(row.bps === 0 || row.volumeMeter !== undefined)
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function arrayValue(value: JsonValue | undefined): readonly JsonValue[] {
|
|
652
|
-
return Array.isArray(value) ? value : [];
|
|
653
|
-
}
|
|
654
|
-
function objectValue(
|
|
655
|
-
value: JsonValue | undefined,
|
|
656
|
-
): Readonly<Record<string, JsonValue>> | undefined {
|
|
657
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
658
|
-
? (value as Readonly<Record<string, JsonValue>>)
|
|
659
|
-
: undefined;
|
|
660
|
-
}
|
|
661
|
-
function nonnegativeInteger(value: string): boolean {
|
|
662
|
-
return /^(0|[1-9][0-9]*)$/.test(value);
|
|
663
|
-
}
|
|
664
|
-
function integerOrZero(value: string): bigint {
|
|
665
|
-
return nonnegativeInteger(value) ? BigInt(value) : 0n;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function scaledDecimal(value: string): bigint {
|
|
669
|
-
const match = /^(0|[1-9][0-9]*)(?:\.([0-9]{1,6}))?$/.exec(value);
|
|
670
|
-
if (!match) throw new Error(`invalid meter reading ${value}`);
|
|
671
|
-
return (
|
|
672
|
-
BigInt(match[1] ?? "0") * 1_000_000n +
|
|
673
|
-
BigInt((match[2] ?? "").padEnd(6, "0"))
|
|
674
|
-
);
|
|
31
|
+
return { actions };
|
|
675
32
|
}
|