@hyperscale0/hsx 1.0.0-rc.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -4
- package/README.md +58 -36
- package/bin/hsx.ts +2 -0
- package/dist/bin/hsx.js +2 -0
- package/dist/bin/hsx.js.map +1 -1
- package/dist/src/ast.d.ts +10 -2
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js +24 -0
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +25 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +9 -5
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +9 -3
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +3 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +124 -94
- package/dist/src/cost.js.map +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +89 -5
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/emit.d.ts +4 -3
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/emit.js +28 -233
- package/dist/src/emit.js.map +1 -1
- package/dist/src/format.js.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ir.d.ts.map +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lsp/server.d.ts +32 -0
- package/dist/src/lsp/server.d.ts.map +1 -0
- package/dist/src/lsp/server.js +391 -0
- package/dist/src/lsp/server.js.map +1 -0
- package/dist/src/modules.d.ts +2 -0
- package/dist/src/modules.d.ts.map +1 -1
- package/dist/src/modules.js +14 -14
- package/dist/src/modules.js.map +1 -1
- package/dist/src/parse.js +3 -3
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts +2 -0
- package/dist/src/std-bundle.d.ts.map +1 -0
- package/dist/src/std-bundle.js +88 -0
- package/dist/src/std-bundle.js.map +1 -0
- package/dist/src/std-library.d.ts +5 -0
- package/dist/src/std-library.d.ts.map +1 -0
- package/dist/src/std-library.js +9 -0
- package/dist/src/std-library.js.map +1 -0
- package/dist/src/typecheck.d.ts.map +1 -1
- package/dist/src/typecheck.js +859 -156
- package/dist/src/typecheck.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/docs/README.md +12 -0
- package/docs/assets/hsx.svg +18 -0
- package/docs/guide/01-first-program.md +2 -2
- package/docs/guide/02-money.md +1 -1
- package/docs/guide/03-instruments.md +44 -3
- package/docs/guide/04-lifecycles.md +18 -3
- package/docs/guide/05-fees-and-splits.md +2 -2
- package/docs/guide/06-schedules.md +1 -1
- package/docs/guide/07-composition.md +8 -2
- package/docs/guide/08-writing-a-module.md +13 -2
- package/docs/guide/09-cost.md +3 -3
- package/docs/guide/10-diagnostics.md +1 -1
- package/docs/llms-full.txt +458 -194
- package/docs/llms.txt +2 -2
- package/docs/playground.md +55 -0
- package/docs/reference/cli.md +5 -3
- package/docs/reference/diagnostics.md +223 -37
- package/docs/reference/grammar.md +2 -2
- package/docs/reference/std/advance.md +2 -2
- package/docs/reference/std/cancellable_booking.md +10 -10
- package/docs/reference/std/captured_payment.md +14 -14
- package/docs/reference/std/conditional_disbursement.md +3 -3
- package/docs/reference/std/credit_facility.md +2 -2
- package/docs/reference/std/held_payment.md +37 -33
- package/docs/reference/std/instant_transfer.md +8 -8
- package/docs/reference/std/metered.md +2 -2
- package/docs/reference/std/pooled_split.md +2 -2
- package/docs/reference/std/premium_forward.md +13 -13
- package/docs/reference/std/reconciled_payout.md +2 -2
- package/docs/reference/std/recurring_collection.md +2 -2
- package/docs/reference/std/rotating_pool.md +2 -2
- package/docs/reference/std/scheduled.md +18 -18
- package/docs/reference/std/security_deposit.md +14 -14
- package/docs/reference/std/settlement_batch.md +2 -2
- package/docs/reference/std/swap.md +18 -18
- package/docs/reference/std/threshold_pool.md +2 -2
- package/docs/reference/std/weighted_distribution.md +2 -2
- package/docs/reference/types.md +1 -1
- package/docs/reference/udl-output.md +2 -3
- package/examples/01-first-program/README.md +1 -1
- package/examples/01-first-program/tip-jar.hsx +1 -1
- package/examples/02-imports-and-archetypes/README.md +1 -4
- package/examples/02-imports-and-archetypes/photo-booth.hsx +1 -1
- package/examples/03-diagnostics/corner-shop-fixed.hsx +1 -1
- package/examples/03-diagnostics/corner-shop.hsx +1 -1
- package/examples/04-complete-product/README.md +0 -3
- package/examples/04-complete-product/study-hall.hsx +1 -1
- package/examples/05-watch-club/README.md +0 -3
- package/examples/05-watch-club/watch-club.hsx +1 -1
- package/examples/README.md +35 -4
- package/examples/advance/advance.hsx +1 -1
- package/examples/advance/advance.udl +10 -5
- package/examples/cancellable_booking/cancellable_booking.hsx +1 -1
- package/examples/cancellable_booking/cancellable_booking.udl +3 -1
- package/examples/captured_payment/captured_payment.hsx +1 -1
- package/examples/captured_payment/captured_payment.udl +4 -1
- package/examples/conditional_disbursement/conditional_disbursement.hsx +1 -1
- package/examples/conditional_disbursement/conditional_disbursement.udl +5 -3
- package/examples/cost-table.json +1602 -382
- package/examples/credit_facility/credit_facility.hsx +1 -1
- package/examples/credit_facility/credit_facility.udl +16 -9
- package/examples/held_payment/held_payment.hsx +1 -1
- package/examples/held_payment/held_payment.udl +38 -3
- package/examples/instant_transfer/instant_transfer.hsx +1 -1
- package/examples/instant_transfer/instant_transfer.udl +11 -5
- package/examples/metered/metered.hsx +1 -1
- package/examples/metered/metered.udl +4 -2
- package/examples/pooled_split/pooled_split.hsx +1 -1
- package/examples/pooled_split/pooled_split.udl +6 -3
- package/examples/premium_forward/premium_forward.hsx +1 -1
- package/examples/premium_forward/premium_forward.udl +10 -4
- package/examples/reconciled_payout/reconciled_payout.hsx +1 -1
- package/examples/reconciled_payout/reconciled_payout.udl +4 -2
- package/examples/recurring_collection/recurring_collection.hsx +1 -1
- package/examples/recurring_collection/recurring_collection.udl +12 -7
- package/examples/rotating_pool/rotating_pool.hsx +1 -1
- package/examples/rotating_pool/rotating_pool.udl +14 -4
- package/examples/scheduled/scheduled.hsx +1 -1
- package/examples/scheduled/scheduled.udl +9 -4
- package/examples/security_deposit/security_deposit.hsx +1 -1
- package/examples/security_deposit/security_deposit.udl +17 -3
- package/examples/settlement_batch/settlement_batch.hsx +1 -1
- package/examples/swap/swap.hsx +1 -1
- package/examples/swap/swap.udl +12 -6
- package/examples/threshold_pool/threshold_pool.hsx +1 -1
- package/examples/threshold_pool/threshold_pool.udl +4 -2
- package/examples/weighted_distribution/weighted_distribution.hsx +1 -1
- package/examples/weighted_distribution/weighted_distribution.udl +4 -2
- package/package.json +15 -6
- package/skills/hsx/SKILL.md +26 -26
- package/src/ast.ts +34 -2
- package/src/cli.ts +32 -13
- package/src/compile.ts +15 -7
- package/src/cost.ts +176 -104
- package/src/diagnostics.ts +89 -4
- package/src/emit.ts +34 -266
- package/src/index.ts +10 -7
- package/src/lsp/server.ts +460 -0
- package/src/modules.ts +21 -13
- package/src/parse.ts +3 -3
- package/src/std-bundle.ts +91 -0
- package/src/std-library.ts +12 -0
- package/src/typecheck.ts +1147 -235
- package/src/version.ts +1 -1
- package/std/SEMANTICS.md +12 -3
- package/std/{settlements → money_flows}/advance.hsx +1 -1
- package/std/{settlements → money_flows}/cancellable_booking.hsx +4 -3
- package/std/{settlements → money_flows}/captured_payment.hsx +35 -6
- package/std/{settlements → money_flows}/conditional_disbursement.hsx +17 -16
- package/std/{settlements → money_flows}/credit_facility.hsx +1 -2
- package/std/{settlements → money_flows}/held_payment.hsx +59 -25
- package/std/money_flows/index.hsx +3 -0
- package/std/{settlements → money_flows}/instant_transfer.hsx +2 -3
- package/std/{settlements → money_flows}/metered.hsx +1 -1
- package/std/{settlements → money_flows}/pooled_split.hsx +3 -3
- package/std/{settlements → money_flows}/premium_forward.hsx +22 -23
- package/std/{settlements → money_flows}/reconciled_payout.hsx +3 -3
- package/std/{settlements → money_flows}/recurring_collection.hsx +1 -1
- package/std/{settlements → money_flows}/rotating_pool.hsx +8 -6
- package/std/{settlements → money_flows}/scheduled.hsx +22 -27
- package/std/{settlements → money_flows}/security_deposit.hsx +38 -14
- package/std/{settlements → money_flows}/settlement_batch.hsx +3 -3
- package/std/{settlements → money_flows}/swap.hsx +41 -16
- package/std/{settlements → money_flows}/threshold_pool.hsx +5 -5
- package/std/{settlements → money_flows}/weighted_distribution.hsx +6 -6
- package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
- package/docs/sessions/2026-09-02-two-instruments.md +0 -41
- package/std/settlements/index.hsx +0 -3
package/src/compile.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The HSX compiler driver: source text in, three-verdict result out.
|
|
3
3
|
*
|
|
4
|
-
* - `valid`: the program lowers cleanly; the IR document
|
|
4
|
+
* - `valid`: the program lowers cleanly; the IR document is ready
|
|
5
5
|
* for the independent checker.
|
|
6
6
|
* - `warning`: the program lowers, and the compiler's lint voice has notes
|
|
7
7
|
* the author should read (the artifacts are still present and usable).
|
|
@@ -16,7 +16,7 @@ import { lineColIn, lineIndex } from "./ast.ts";
|
|
|
16
16
|
import {
|
|
17
17
|
buildCostManifest,
|
|
18
18
|
type UdlCostManifest,
|
|
19
|
-
type
|
|
19
|
+
type UdlCostTables,
|
|
20
20
|
} from "./cost.ts";
|
|
21
21
|
import { lowerGeneralProgram, type OriginMapEntry } from "./emit.ts";
|
|
22
22
|
import {
|
|
@@ -41,6 +41,8 @@ export interface CompileDiagnostic {
|
|
|
41
41
|
readonly message: string;
|
|
42
42
|
/** Source offsets in UTF-16 code units, matching parser and checker spans. */
|
|
43
43
|
readonly span: { readonly end: number; readonly start: number };
|
|
44
|
+
/** UTF-8 byte span in root source, omitted when referring to an imported module. */
|
|
45
|
+
readonly byteSpan?: { readonly end: number; readonly start: number };
|
|
44
46
|
readonly code?: string;
|
|
45
47
|
readonly fix?: string;
|
|
46
48
|
/** Canonical UDL path when lowering rejected an emitted clause. */
|
|
@@ -55,8 +57,6 @@ export interface CompileDiagnostic {
|
|
|
55
57
|
interface CompileArtifacts {
|
|
56
58
|
/** The canonical UDL document. */
|
|
57
59
|
readonly document: Json;
|
|
58
|
-
/** The congruent Business Frame. */
|
|
59
|
-
readonly frame: Json;
|
|
60
60
|
/** Canonical UDL paths bound to their narrowest authored source terms. */
|
|
61
61
|
readonly originMap: readonly CompileOriginMapEntry[];
|
|
62
62
|
/** Deterministic compile-time cost, pinned to one versioned table. */
|
|
@@ -65,7 +65,8 @@ interface CompileArtifacts {
|
|
|
65
65
|
|
|
66
66
|
export interface CompileOptions extends HsxCompilerHost {
|
|
67
67
|
readonly composesCatalogBlueprint?: boolean;
|
|
68
|
-
|
|
68
|
+
/** One table, or one per billing currency; the program's money fields pick. */
|
|
69
|
+
readonly costTable?: UdlCostTables;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export interface CompileOriginMapEntry {
|
|
@@ -115,7 +116,15 @@ export function compile(
|
|
|
115
116
|
const position = origin ?? lineColIn(lines, span.start);
|
|
116
117
|
diagnostics.push({
|
|
117
118
|
column: position.column,
|
|
118
|
-
...(origin
|
|
119
|
+
...(origin
|
|
120
|
+
? { file: origin.moduleName }
|
|
121
|
+
: {
|
|
122
|
+
byteSpan: {
|
|
123
|
+
end:
|
|
124
|
+
byteOffsets[span.end] ?? byteOffsets[byteOffsets.length - 1]!,
|
|
125
|
+
start: byteOffsets[span.start] ?? 0,
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
119
128
|
line: position.line,
|
|
120
129
|
message,
|
|
121
130
|
span: { ...span },
|
|
@@ -215,7 +224,6 @@ export function compile(
|
|
|
215
224
|
artifacts: {
|
|
216
225
|
document: lowered.value.document,
|
|
217
226
|
costManifest: cost.manifest,
|
|
218
|
-
frame: lowered.value.frame,
|
|
219
227
|
originMap: lowered.value.originMap.map(compileOrigin),
|
|
220
228
|
},
|
|
221
229
|
diagnostics,
|
package/src/cost.ts
CHANGED
|
@@ -3,10 +3,17 @@ import {
|
|
|
3
3
|
type UdlDocument,
|
|
4
4
|
type UdlEffectKind,
|
|
5
5
|
} from "@hyperscale0/udl";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
GeneralDiagnostic,
|
|
8
|
+
JsonValue,
|
|
9
|
+
TypedField,
|
|
10
|
+
TypedProgram,
|
|
11
|
+
} from "./ir.ts";
|
|
7
12
|
|
|
8
13
|
export type HsxEffectKind = UdlEffectKind;
|
|
9
14
|
|
|
15
|
+
const CURRENCY = /^[A-Z]{3}$/;
|
|
16
|
+
|
|
10
17
|
export interface UdlCostTable {
|
|
11
18
|
readonly version: string;
|
|
12
19
|
/** Digest of the effective prices and fixed anchors after overrides. */
|
|
@@ -72,16 +79,20 @@ export interface UdlCostManifest {
|
|
|
72
79
|
};
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
/** One cost table, or one per billing currency the host prices. */
|
|
83
|
+
export type UdlCostTables = UdlCostTable | readonly UdlCostTable[];
|
|
84
|
+
|
|
75
85
|
export type CostManifestResult =
|
|
76
86
|
| { readonly diagnostics: readonly GeneralDiagnostic[]; readonly ok: false }
|
|
77
87
|
| { readonly manifest: UdlCostManifest; readonly ok: true };
|
|
78
88
|
|
|
79
89
|
export function buildCostManifest(
|
|
80
90
|
program: TypedProgram,
|
|
81
|
-
|
|
91
|
+
tables: UdlCostTables | undefined,
|
|
82
92
|
composesCatalogBlueprint: boolean,
|
|
83
93
|
): CostManifestResult {
|
|
84
|
-
|
|
94
|
+
const priced = tables === undefined ? [] : [tables].flat();
|
|
95
|
+
if (priced.length === 0) {
|
|
85
96
|
return {
|
|
86
97
|
diagnostics: [
|
|
87
98
|
{
|
|
@@ -97,13 +108,16 @@ export function buildCostManifest(
|
|
|
97
108
|
};
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
|
|
111
|
+
const malformed = priced.find(
|
|
112
|
+
(candidate) => !CURRENCY.test(candidate.currency),
|
|
113
|
+
);
|
|
114
|
+
if (malformed) {
|
|
101
115
|
return {
|
|
102
116
|
diagnostics: [
|
|
103
117
|
{
|
|
104
118
|
code: "HSX1303",
|
|
105
119
|
fix: "name one three-letter uppercase billing currency on the cost table",
|
|
106
|
-
message: `cost table ${
|
|
120
|
+
message: `cost table ${malformed.version} has invalid billing currency ${malformed.currency}`,
|
|
107
121
|
severity: "error",
|
|
108
122
|
span: program.origin,
|
|
109
123
|
},
|
|
@@ -113,65 +127,39 @@ export function buildCostManifest(
|
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
const diagnostics: GeneralDiagnostic[] = [];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
...(rowsBySignature.get(row.signature) ?? []),
|
|
120
|
-
row,
|
|
121
|
-
]);
|
|
122
|
-
}
|
|
130
|
+
const table = ledgerCostTable(program, priced, diagnostics);
|
|
131
|
+
if (!table) return { diagnostics, ok: false };
|
|
132
|
+
const rowsBySignature = indexCostRows(table);
|
|
123
133
|
const actions: UdlCostManifest["actions"][number][] = [];
|
|
124
134
|
for (const instrument of program.instruments) {
|
|
125
135
|
for (const action of instrument.actions) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
|
|
154
|
-
if (row.payer === "product") productPerEvent += eventTotal;
|
|
155
|
-
else endCustomerPerEvent += eventTotal;
|
|
156
|
-
components.push({
|
|
157
|
-
bps: row.bps,
|
|
158
|
-
count,
|
|
159
|
-
meter: row.meter,
|
|
160
|
-
payer: row.payer,
|
|
161
|
-
perEventMinor: row.perEventMinor,
|
|
162
|
-
settlement: row.settlement,
|
|
163
|
-
signature,
|
|
164
|
-
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
actions.push({
|
|
169
|
-
action: action.name,
|
|
170
|
-
components,
|
|
171
|
-
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
172
|
-
instrument: instrument.id,
|
|
173
|
-
perEventMinor: productPerEvent.toString(),
|
|
174
|
-
});
|
|
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
|
+
);
|
|
175
163
|
}
|
|
176
164
|
}
|
|
177
165
|
const fixed = fixedCost(
|
|
@@ -184,6 +172,52 @@ export function buildCostManifest(
|
|
|
184
172
|
return { manifest: manifest(table, fixed, actions), ok: true };
|
|
185
173
|
}
|
|
186
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
|
+
|
|
187
221
|
export function computeUdlFixedCost(
|
|
188
222
|
document: UdlDocument,
|
|
189
223
|
table: UdlCostTable,
|
|
@@ -205,25 +239,15 @@ export function buildUdlCostManifest(
|
|
|
205
239
|
table: UdlCostTable,
|
|
206
240
|
composesCatalogBlueprint: boolean,
|
|
207
241
|
): UdlCostManifest {
|
|
208
|
-
if (
|
|
242
|
+
if (!CURRENCY.test(table.currency)) {
|
|
209
243
|
throw new Error(
|
|
210
244
|
`cost table ${table.version} has invalid billing currency ${table.currency}`,
|
|
211
245
|
);
|
|
212
246
|
}
|
|
213
|
-
const rowsBySignature =
|
|
214
|
-
for (const row of table.rows) {
|
|
215
|
-
rowsBySignature.set(row.signature, [
|
|
216
|
-
...(rowsBySignature.get(row.signature) ?? []),
|
|
217
|
-
row,
|
|
218
|
-
]);
|
|
219
|
-
}
|
|
247
|
+
const rowsBySignature = indexCostRows(table);
|
|
220
248
|
const actions: UdlCostManifest["actions"][number][] = [];
|
|
221
249
|
for (const instrument of document.instruments) {
|
|
222
250
|
for (const [actionName, action] of Object.entries(instrument.actions)) {
|
|
223
|
-
const components: UdlCostManifest["actions"][number]["components"][number][] =
|
|
224
|
-
[];
|
|
225
|
-
let productPerEvent = 0n;
|
|
226
|
-
let endCustomerPerEvent = 0n;
|
|
227
251
|
const effects = (action.effects ?? {}) as Readonly<
|
|
228
252
|
Partial<
|
|
229
253
|
Record<
|
|
@@ -232,39 +256,24 @@ export function buildUdlCostManifest(
|
|
|
232
256
|
>
|
|
233
257
|
>
|
|
234
258
|
>;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
259
|
+
actions.push(
|
|
260
|
+
priceAction(
|
|
261
|
+
instrument.id,
|
|
262
|
+
actionName,
|
|
263
|
+
effectCounts(effects),
|
|
264
|
+
rowsBySignature,
|
|
265
|
+
(issue) => {
|
|
266
|
+
if (issue.kind === "missing_price") {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`${instrument.id}.${actionName} has unpriced effect signature ${issue.signature}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
243
271
|
throw new Error(
|
|
244
|
-
`cost table ${table.version} has an invalid ${signature} price`,
|
|
272
|
+
`cost table ${table.version} has an invalid ${issue.signature} price`,
|
|
245
273
|
);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
components.push({
|
|
250
|
-
bps: row.bps,
|
|
251
|
-
count,
|
|
252
|
-
meter: row.meter,
|
|
253
|
-
payer: row.payer,
|
|
254
|
-
perEventMinor: row.perEventMinor,
|
|
255
|
-
settlement: row.settlement,
|
|
256
|
-
signature,
|
|
257
|
-
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
actions.push({
|
|
262
|
-
action: actionName,
|
|
263
|
-
components,
|
|
264
|
-
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
265
|
-
instrument: instrument.id,
|
|
266
|
-
perEventMinor: productPerEvent.toString(),
|
|
267
|
-
});
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
);
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
return manifest(
|
|
@@ -295,6 +304,69 @@ export function evaluateUdlCostManifest(
|
|
|
295
304
|
return total.toString();
|
|
296
305
|
}
|
|
297
306
|
|
|
307
|
+
interface ActionPricingIssue {
|
|
308
|
+
readonly kind: "invalid_price" | "missing_price";
|
|
309
|
+
readonly signature: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function indexCostRows(
|
|
313
|
+
table: UdlCostTable,
|
|
314
|
+
): ReadonlyMap<string, readonly UdlCostTable["rows"][number][]> {
|
|
315
|
+
const rowsBySignature = new Map<string, UdlCostTable["rows"][number][]>();
|
|
316
|
+
for (const row of table.rows) {
|
|
317
|
+
const existing = rowsBySignature.get(row.signature);
|
|
318
|
+
if (existing) existing.push(row);
|
|
319
|
+
else rowsBySignature.set(row.signature, [row]);
|
|
320
|
+
}
|
|
321
|
+
return rowsBySignature;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function priceAction(
|
|
325
|
+
instrument: string,
|
|
326
|
+
action: string,
|
|
327
|
+
counts: Iterable<readonly [string, number]>,
|
|
328
|
+
rowsBySignature: ReadonlyMap<string, readonly UdlCostTable["rows"][number][]>,
|
|
329
|
+
onIssue: (issue: ActionPricingIssue) => void,
|
|
330
|
+
): UdlCostManifest["actions"][number] {
|
|
331
|
+
const components: UdlCostManifest["actions"][number]["components"][number][] =
|
|
332
|
+
[];
|
|
333
|
+
let productPerEvent = 0n;
|
|
334
|
+
let endCustomerPerEvent = 0n;
|
|
335
|
+
for (const [signature, count] of counts) {
|
|
336
|
+
const rows = rowsBySignature.get(signature);
|
|
337
|
+
if (!rows || rows.length === 0) {
|
|
338
|
+
onIssue({ kind: "missing_price", signature });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
for (const row of rows) {
|
|
342
|
+
if (!validPrice(row)) {
|
|
343
|
+
onIssue({ kind: "invalid_price", signature });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
|
|
347
|
+
if (row.payer === "product") productPerEvent += eventTotal;
|
|
348
|
+
else endCustomerPerEvent += eventTotal;
|
|
349
|
+
components.push({
|
|
350
|
+
bps: row.bps,
|
|
351
|
+
count,
|
|
352
|
+
meter: row.meter,
|
|
353
|
+
payer: row.payer,
|
|
354
|
+
perEventMinor: row.perEventMinor,
|
|
355
|
+
settlement: row.settlement,
|
|
356
|
+
signature,
|
|
357
|
+
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
action,
|
|
363
|
+
components,
|
|
364
|
+
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
365
|
+
instrument,
|
|
366
|
+
perEventMinor: productPerEvent.toString(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
298
370
|
function manifest(
|
|
299
371
|
table: UdlCostTable,
|
|
300
372
|
fixed: UdlCostManifest["fixed"],
|
package/src/diagnostics.ts
CHANGED
|
@@ -9,11 +9,16 @@ export interface HsxDiagnosticCatalogEntry {
|
|
|
9
9
|
|
|
10
10
|
const instrument = (
|
|
11
11
|
body: string,
|
|
12
|
+
fields = "",
|
|
12
13
|
): string => `program catalog_probe "Catalog probe"
|
|
13
14
|
instrument probe {
|
|
14
|
-
|
|
15
|
+
agent_description: "Probe instrument for catalog diagnostics.";
|
|
16
|
+
fields { ${fields} }
|
|
15
17
|
lifecycle { states created; initial created; }
|
|
16
|
-
action create {
|
|
18
|
+
action create {
|
|
19
|
+
agent_description: "Create a probe instance.";
|
|
20
|
+
steps: [];
|
|
21
|
+
}
|
|
17
22
|
${body}
|
|
18
23
|
}`;
|
|
19
24
|
|
|
@@ -201,8 +206,8 @@ instrument probe = template(value: "one", value: "two")`,
|
|
|
201
206
|
{
|
|
202
207
|
code: "HSX1022",
|
|
203
208
|
stage: "typecheck",
|
|
204
|
-
title: "Authored instrument
|
|
205
|
-
fix: "
|
|
209
|
+
title: "Authored instrument collides with published catalog",
|
|
210
|
+
fix: "Rename the authored instrument to avoid colliding with published catalog instruments.",
|
|
206
211
|
// This diagnostic requires a compiler-host published catalog.
|
|
207
212
|
example: null,
|
|
208
213
|
reason: "This refusal requires a compiler-host published catalog.",
|
|
@@ -214,6 +219,44 @@ instrument probe = template(value: "one", value: "two")`,
|
|
|
214
219
|
fix: "List declared, non-optional field names once each.",
|
|
215
220
|
example: instrument("required: missing;"),
|
|
216
221
|
},
|
|
222
|
+
{
|
|
223
|
+
code: "HSX1024",
|
|
224
|
+
stage: "typecheck",
|
|
225
|
+
title: "Decision party has no matching account binding",
|
|
226
|
+
fix: "Bind the allowed party to an account field and use that binding in the decision action.",
|
|
227
|
+
example: `program decision_binding "Decision binding"
|
|
228
|
+
party buyer: person
|
|
229
|
+
party seller: business
|
|
230
|
+
port approve { allowed: [buyer] }
|
|
231
|
+
instrument gated(decision: condition) {
|
|
232
|
+
fields {}
|
|
233
|
+
parties { payer: seller }
|
|
234
|
+
lifecycle { states created done; initial created; on approve: created -> done; }
|
|
235
|
+
action create { steps: []; }
|
|
236
|
+
action [decision] { steps: []; port { allowed_parties: [payer]; } }
|
|
237
|
+
}
|
|
238
|
+
instrument gate = gated(decision: port approve)`,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
code: "HSX1025",
|
|
242
|
+
stage: "typecheck",
|
|
243
|
+
title: "Unknown decision port shape type",
|
|
244
|
+
fix: "Declare the shape field as text, money(CUR), date, integer, boolean, account(CUR), ref<instrument_id>, id(instrument_id), bps, or percent.",
|
|
245
|
+
example: `program catalog_probe "Catalog probe"
|
|
246
|
+
import { security_deposit } from "std/money_flows"
|
|
247
|
+
party buyer: person
|
|
248
|
+
party seller: business
|
|
249
|
+
settlement hold = security_deposit {
|
|
250
|
+
payer: buyer
|
|
251
|
+
holder: seller
|
|
252
|
+
amount: depositAmount: money(SAR)
|
|
253
|
+
claim: port report_damage
|
|
254
|
+
claim_amount: { field: damageAmount, bound: depositAmount, remainder: return }
|
|
255
|
+
return: port pass_inspection
|
|
256
|
+
}
|
|
257
|
+
port report_damage { allowed: [seller] shape { damageAmount: money(SAR) } }
|
|
258
|
+
port pass_inspection { allowed: [seller] shape { invalidField: mystery_type } }`,
|
|
259
|
+
},
|
|
217
260
|
{
|
|
218
261
|
code: "HSX1101",
|
|
219
262
|
stage: "typecheck",
|
|
@@ -298,6 +341,20 @@ action pay { computes remainder rest { amount_ref: total; on_zero: refuse; total
|
|
|
298
341
|
reason:
|
|
299
342
|
"This refusal requires an invalid compiler-host cost-table currency.",
|
|
300
343
|
},
|
|
344
|
+
{
|
|
345
|
+
code: "HSX1304",
|
|
346
|
+
stage: "typecheck",
|
|
347
|
+
title: "Unpriced ledger currency",
|
|
348
|
+
fix: "Move money in a currency the shipped cost tables price.",
|
|
349
|
+
example: instrument("", "amount: money<JPY>;"),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
code: "HSX1305",
|
|
353
|
+
stage: "typecheck",
|
|
354
|
+
title: "More than one ledger currency",
|
|
355
|
+
fix: "Move all money in one currency; a program bills in one ledger currency.",
|
|
356
|
+
example: instrument("", "amount: money<SAR>; fee: money<USD>;"),
|
|
357
|
+
},
|
|
301
358
|
{
|
|
302
359
|
code: "HSX1401",
|
|
303
360
|
stage: "typecheck",
|
|
@@ -411,6 +468,34 @@ instrument probe = template()`,
|
|
|
411
468
|
example: `program catalog_probe "Catalog probe"
|
|
412
469
|
export instrument template() { fields {}; lifecycle { states created; initial created; }; action create { steps: []; }; }
|
|
413
470
|
instrument probe = template() { fields: {}; }`,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
code: "HSX1508",
|
|
474
|
+
stage: "typecheck",
|
|
475
|
+
title: "Invalid action-level port syntax",
|
|
476
|
+
fix: "Use allowed_parties: [...] inside an action-level port clause; allowed: is for top-level port declarations.",
|
|
477
|
+
example: `program catalog_probe "Catalog probe"
|
|
478
|
+
party approver: person
|
|
479
|
+
instrument probe {
|
|
480
|
+
fields {}
|
|
481
|
+
lifecycle { states created; initial created; }
|
|
482
|
+
action create {
|
|
483
|
+
steps: [];
|
|
484
|
+
port { allowed: [approver]; }
|
|
485
|
+
}
|
|
486
|
+
}`,
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
code: "HSX1509",
|
|
490
|
+
stage: "typecheck",
|
|
491
|
+
title: "Callable action without an agent description",
|
|
492
|
+
fix: 'Add agent_description: "..." to the instrument and to every action a caller can reach; composer.check refuses the program without them.',
|
|
493
|
+
example: `program catalog_probe "Catalog probe"
|
|
494
|
+
instrument probe {
|
|
495
|
+
fields {}
|
|
496
|
+
lifecycle { states created; initial created; }
|
|
497
|
+
action create { steps: []; }
|
|
498
|
+
}`,
|
|
414
499
|
},
|
|
415
500
|
{
|
|
416
501
|
code: "HSX1601",
|