@hyperscale0/hsx 1.0.337 → 1.0.339
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/dist/src/ast.d.ts +1 -0
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js.map +1 -1
- package/dist/src/attachment-economics.d.ts +37 -0
- package/dist/src/attachment-economics.d.ts.map +1 -1
- package/dist/src/attachment-economics.js +183 -3
- package/dist/src/attachment-economics.js.map +1 -1
- package/dist/src/binding-contract.d.ts.map +1 -1
- package/dist/src/binding-contract.js +10 -4
- package/dist/src/binding-contract.js.map +1 -1
- package/dist/src/catalog-program.d.ts +1 -1
- package/dist/src/catalog-program.d.ts.map +1 -1
- package/dist/src/catalog-program.js +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +133 -41
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +1 -1
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +3 -1
- package/dist/src/headers.js.map +1 -1
- package/dist/src/language.d.ts.map +1 -1
- package/dist/src/language.js +1 -0
- package/dist/src/language.js.map +1 -1
- package/dist/src/parse.js +13 -1
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +6 -6
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/template-guide.d.ts.map +1 -1
- package/dist/src/template-guide.js +49 -18
- package/dist/src/template-guide.js.map +1 -1
- package/docs/README.md +5 -2
- package/examples/camera-rental.hsx +6 -1
- package/package.json +4 -4
- package/src/ast.ts +7 -1
- package/src/attachment-economics.ts +230 -2
- package/src/binding-contract.ts +13 -6
- package/src/catalog-program.ts +1 -1
- package/src/compile.ts +185 -58
- package/src/examples-bundle.ts +1 -1
- package/src/headers.ts +6 -1
- package/src/language.ts +1 -0
- package/src/parse.ts +16 -1
- package/src/std-bundle.ts +6 -6
- package/src/template-guide.ts +74 -22
- package/std/collections.hsx +4 -4
- package/std/financing.hsx +461 -266
- package/std/insurance.hsx +21 -5
- package/std/lending.hsx +13 -9
- package/std/money.hsx +148 -38
- package/std/reporting.hsx +6 -6
package/dist/src/compile.js
CHANGED
|
@@ -5,7 +5,7 @@ import { validateUdl, resolveField, sameAccount, sameObjectField, subjectPartyRo
|
|
|
5
5
|
import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
|
|
6
6
|
import { tunableBounds } from "./tunables.js";
|
|
7
7
|
import { parseProgram } from "./parse.js";
|
|
8
|
-
import { applyAttachmentEconomics, purposeWarnings, } from "./attachment-economics.js";
|
|
8
|
+
import { applyAttachmentEconomics, purposeWarnings, samePartyMoves, unboundEconomicSources, } from "./attachment-economics.js";
|
|
9
9
|
import { parseHeader } from "./header-source.js";
|
|
10
10
|
import { lineColAt, } from "./ast.js";
|
|
11
11
|
import { bundledStandardLibrary } from "./std-library.js";
|
|
@@ -200,6 +200,24 @@ function decimal(raw, scale, expr) {
|
|
|
200
200
|
return (BigInt(whole) * 10n ** BigInt(scale) +
|
|
201
201
|
BigInt(fraction.padEnd(scale, "0") || "0")).toString();
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A calendar period as ISO-8601 text: `1 month`, `2 weeks`, `7d` or `P1M`.
|
|
205
|
+
* Days, weeks, months and years only, the units Stripe and Shopify bill in.
|
|
206
|
+
*/
|
|
207
|
+
function periodText(expr) {
|
|
208
|
+
if (!["duration", "text", "name"].includes(expr.kind))
|
|
209
|
+
return undefined;
|
|
210
|
+
const value = expr.value.trim();
|
|
211
|
+
const units = { d: "D", w: "W", m: "M", y: "Y" };
|
|
212
|
+
const words = /^([1-9]\d{0,3}) ?(day|week|month|year)s?$/.exec(value);
|
|
213
|
+
const short = /^([1-9]\d{0,3})(d|w)$/.exec(value);
|
|
214
|
+
const iso = /^P(?!$)(?:[1-9]\d{0,3}Y)?(?:[1-9]\d{0,3}M)?(?:[1-9]\d{0,3}W)?(?:[1-9]\d{0,3}D)?$/.exec(value);
|
|
215
|
+
if (words)
|
|
216
|
+
return `P${words[1]}${units[words[2][0]]}`;
|
|
217
|
+
if (short)
|
|
218
|
+
return `P${short[1]}${units[short[2]]}`;
|
|
219
|
+
return iso ? value : undefined;
|
|
220
|
+
}
|
|
203
221
|
function literal(expr, expectedType) {
|
|
204
222
|
if (expr.kind === "text" && expectedType === "duration")
|
|
205
223
|
return literal({ ...expr, kind: "duration" });
|
|
@@ -501,7 +519,9 @@ export function compile(source, options = {}) {
|
|
|
501
519
|
}
|
|
502
520
|
return { module: moduleName, exportPath, revision };
|
|
503
521
|
};
|
|
504
|
-
const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin
|
|
522
|
+
const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin,
|
|
523
|
+
/** Declared tunables, true where the author bound them. */
|
|
524
|
+
inheritedTunables = new Map()) => {
|
|
505
525
|
const resolveFamilyInstruments = (family, currentInstId, expr) => {
|
|
506
526
|
const found = new Set();
|
|
507
527
|
if (currentInstId && familyDeclaration) {
|
|
@@ -635,13 +655,16 @@ export function compile(source, options = {}) {
|
|
|
635
655
|
suppliedEntries.set(entry.key, entry);
|
|
636
656
|
}
|
|
637
657
|
const environment = new Map(inherited);
|
|
658
|
+
// `when every is set` asks whether the author bound a tunable; a
|
|
659
|
+
// default leaves it unset.
|
|
660
|
+
const tunables = new Map(inheritedTunables);
|
|
638
661
|
const parameterErrors = [];
|
|
639
662
|
for (const param of decl.parameters) {
|
|
640
663
|
const type = param.value.kind === "default" ? param.value.type : param.value;
|
|
641
664
|
const fallback = param.value.kind === "default" ? param.value.value : undefined;
|
|
642
665
|
const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
|
|
643
666
|
const partyParameter = attachmentInfo && typeName === "party";
|
|
644
|
-
const optional = type.kind === "type" && type.optional;
|
|
667
|
+
const optional = (type.kind === "type" || type.kind === "call") && !!type.optional;
|
|
645
668
|
// An optional party stays unset unless the attachment binds it, so a
|
|
646
669
|
// program party that shares its name does not switch it on.
|
|
647
670
|
const byName = partyParameter &&
|
|
@@ -650,6 +673,7 @@ export function compile(source, options = {}) {
|
|
|
650
673
|
document.parties[param.key])
|
|
651
674
|
? { kind: "name", value: param.key, span: origin }
|
|
652
675
|
: undefined;
|
|
676
|
+
tunables.set(param.key, supplied.has(param.key) || !!byName);
|
|
653
677
|
const actual = supplied.get(param.key) ??
|
|
654
678
|
byName ??
|
|
655
679
|
(fallback && {
|
|
@@ -699,9 +723,10 @@ export function compile(source, options = {}) {
|
|
|
699
723
|
}
|
|
700
724
|
for (const dependency of dependencies) {
|
|
701
725
|
const selected = environment.get(dependency.when);
|
|
702
|
-
|
|
703
|
-
selected.value === dependency.is
|
|
704
|
-
|
|
726
|
+
const holds = enums.has(dependency.when)
|
|
727
|
+
? selected?.kind === "name" && selected.value === dependency.is
|
|
728
|
+
: tunables.get(dependency.when) === (dependency.is === "set");
|
|
729
|
+
if (holds && !environment.has(dependency.binding)) {
|
|
705
730
|
throw new CompileFailure({
|
|
706
731
|
code: "HSX1001",
|
|
707
732
|
message: dependency.message.replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id),
|
|
@@ -812,7 +837,8 @@ export function compile(source, options = {}) {
|
|
|
812
837
|
const target = templates.get(type);
|
|
813
838
|
const required = target?.parameters
|
|
814
839
|
.filter((p) => p.value.kind !== "default" &&
|
|
815
|
-
!(p.value.kind === "type"
|
|
840
|
+
!((p.value.kind === "type" || p.value.kind === "call") &&
|
|
841
|
+
p.value.optional))
|
|
816
842
|
.map((p) => p.key) ?? [];
|
|
817
843
|
fail({ span: origin }, `Attachment \`${attachmentInfo.attachmentName}\` needs a \`${parameter}\` binding. ${matches.length === 0
|
|
818
844
|
? `No \`${type}\` attachment exists on \`${attachmentInfo.subjectKindId}\`.`
|
|
@@ -895,6 +921,10 @@ export function compile(source, options = {}) {
|
|
|
895
921
|
(v.kind === "text" || v.kind === "name") &&
|
|
896
922
|
/^P/.test(v.value))
|
|
897
923
|
v = { ...v, kind: "duration" };
|
|
924
|
+
// A period lowers as ISO text, so `after` steps calendar months.
|
|
925
|
+
const period = type === "period" ? periodText(v) : undefined;
|
|
926
|
+
if (period)
|
|
927
|
+
v = { kind: "text", value: period, span: v.span };
|
|
898
928
|
environment.set(param.key, v);
|
|
899
929
|
if (type === "enum" && t.kind === "call") {
|
|
900
930
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -982,6 +1012,10 @@ export function compile(source, options = {}) {
|
|
|
982
1012
|
span: v.span,
|
|
983
1013
|
});
|
|
984
1014
|
}
|
|
1015
|
+
else if (type === "period") {
|
|
1016
|
+
if (!period)
|
|
1017
|
+
fail(v, `${param.key} needs a calendar period`, `write ${param.key}: 1 month, 2 weeks, 1 year or 7 days`);
|
|
1018
|
+
}
|
|
985
1019
|
else if (type === "fee" || type === "split" || type === "policy") {
|
|
986
1020
|
if (v.kind !== "block")
|
|
987
1021
|
fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
|
|
@@ -1067,6 +1101,27 @@ export function compile(source, options = {}) {
|
|
|
1067
1101
|
throw new CompileFailure(parameterErrors[0]);
|
|
1068
1102
|
}
|
|
1069
1103
|
const body = entries(decl.body);
|
|
1104
|
+
// A ceiling caps a tunable by the value of an enum tunable, such as the
|
|
1105
|
+
// longest term per loan kind. An unset tunable takes the ceiling; a
|
|
1106
|
+
// supplied one may lower it, never raise it.
|
|
1107
|
+
for (const ceiling of asBlock(body.get("ceilings")).entries) {
|
|
1108
|
+
const table = entries(asBlock(ceiling.value));
|
|
1109
|
+
const by = table.get("by");
|
|
1110
|
+
const selected = by && environment.get(text(by));
|
|
1111
|
+
if (!by || selected?.kind !== "name")
|
|
1112
|
+
fail(ceiling, `ceiling ${ceiling.key} needs by: an enum tunable`, "write by: kind and one maximum per enum value");
|
|
1113
|
+
const maximum = table.get(selected.value);
|
|
1114
|
+
if (maximum?.kind !== "number")
|
|
1115
|
+
fail(ceiling, `ceiling ${ceiling.key} has no maximum for ${selected.value}`, `add ${selected.value}: number to the ceiling`);
|
|
1116
|
+
if (!tunables.get(ceiling.key)) {
|
|
1117
|
+
environment.set(ceiling.key, { ...maximum, span: origin });
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const supplied = environment.get(ceiling.key);
|
|
1121
|
+
if (supplied.kind !== "number" ||
|
|
1122
|
+
BigInt(supplied.value) > BigInt(maximum.value))
|
|
1123
|
+
fail(supplied, `${ceiling.key} for ${selected.value} is at most ${maximum.value}`, `choose ${ceiling.key} of ${maximum.value} or less, or omit it`);
|
|
1124
|
+
}
|
|
1070
1125
|
for (const constraint of asBlock(body.get("constraints")).entries) {
|
|
1071
1126
|
const rule = constraint.value;
|
|
1072
1127
|
if (rule.kind !== "call" ||
|
|
@@ -1076,6 +1131,9 @@ export function compile(source, options = {}) {
|
|
|
1076
1131
|
const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
|
|
1077
1132
|
if (!left || !right)
|
|
1078
1133
|
fail(rule, "constraint needs two declared tunables", "name the compared tunables");
|
|
1134
|
+
// A runtime value is checked when the agreement is created.
|
|
1135
|
+
if ([left, right].some((expr) => expr.kind === "name" && expr.value === "runtime"))
|
|
1136
|
+
continue;
|
|
1079
1137
|
const numericKind = (expr) => expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
|
|
1080
1138
|
? "duration"
|
|
1081
1139
|
: expr.kind;
|
|
@@ -1099,6 +1157,7 @@ export function compile(source, options = {}) {
|
|
|
1099
1157
|
"summary",
|
|
1100
1158
|
"invariants",
|
|
1101
1159
|
"constraints",
|
|
1160
|
+
"ceilings",
|
|
1102
1161
|
"dependencies",
|
|
1103
1162
|
"parameterDiagnostics",
|
|
1104
1163
|
"reports",
|
|
@@ -1289,9 +1348,52 @@ export function compile(source, options = {}) {
|
|
|
1289
1348
|
: value.value;
|
|
1290
1349
|
return literal(value);
|
|
1291
1350
|
};
|
|
1351
|
+
const branchMatches = (entry) => {
|
|
1352
|
+
const [, tunable, relation, choice] = entry.key.split(" ");
|
|
1353
|
+
// `when every is set` holds when the author bound the tunable, so an
|
|
1354
|
+
// optional tunable and a defaulted one both read as unset.
|
|
1355
|
+
if (relation === "is" &&
|
|
1356
|
+
(choice === "set" || choice === "unset") &&
|
|
1357
|
+
tunables.has(tunable) &&
|
|
1358
|
+
!enums.has(tunable))
|
|
1359
|
+
return tunables.get(tunable) === (choice === "set");
|
|
1360
|
+
const binding = environment.get(tunable);
|
|
1361
|
+
if (!binding)
|
|
1362
|
+
fail(entry, `unknown branch tunable ${tunable}`, "name an enum, reference or optional tunable declared by this header");
|
|
1363
|
+
if (relation === "has") {
|
|
1364
|
+
// Inspect declarations, not lowering order: a bound object may
|
|
1365
|
+
// appear after the instrument that asks about its fields.
|
|
1366
|
+
const bound = resolve(binding);
|
|
1367
|
+
const [root, ...children] = text(bound).split(".");
|
|
1368
|
+
const assignment = assignments.get(root);
|
|
1369
|
+
let target = assignment
|
|
1370
|
+
? templates.get(assignment.target)?.body
|
|
1371
|
+
: program.decls
|
|
1372
|
+
.filter((decl) => decl.kind === "instrument")
|
|
1373
|
+
.find((decl) => decl.name === root)?.body;
|
|
1374
|
+
for (const child of children) {
|
|
1375
|
+
const record = target
|
|
1376
|
+
? entries(asBlock(entries(target).get("records"))).get(child)
|
|
1377
|
+
: undefined;
|
|
1378
|
+
target = record ? asBlock(record) : undefined;
|
|
1379
|
+
}
|
|
1380
|
+
if (!target)
|
|
1381
|
+
fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
|
|
1382
|
+
return entries(asBlock(entries(target).get("fields"))).has(choice);
|
|
1383
|
+
}
|
|
1384
|
+
if (!enums.get(tunable)?.includes(choice))
|
|
1385
|
+
fail(entry, `unknown enum branch ${choice}`, "use a declared enum value, or set or unset");
|
|
1386
|
+
return text(binding) === choice;
|
|
1387
|
+
};
|
|
1388
|
+
// Fields and inputs take `when` branches too; a branch holds field rows.
|
|
1389
|
+
const branchRows = (block) => block.entries.flatMap((row) => row.key.startsWith("when ")
|
|
1390
|
+
? branchMatches(row)
|
|
1391
|
+
? branchRows(asBlock(row.value))
|
|
1392
|
+
: []
|
|
1393
|
+
: [row]);
|
|
1292
1394
|
const lowerFields = (block) => {
|
|
1293
1395
|
const result = [];
|
|
1294
|
-
for (const row of block
|
|
1396
|
+
for (const row of branchRows(block)) {
|
|
1295
1397
|
const { type: t } = fieldType(row);
|
|
1296
1398
|
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
1297
1399
|
const f = lowerFieldShape(row, resolve);
|
|
@@ -1476,6 +1578,15 @@ export function compile(source, options = {}) {
|
|
|
1476
1578
|
milliseconds: args[1],
|
|
1477
1579
|
direction: constant.name,
|
|
1478
1580
|
});
|
|
1581
|
+
// after(start, 1 month, 3) steps calendar periods from start.
|
|
1582
|
+
else if (constant.name === "after" && args.length === 3)
|
|
1583
|
+
calculations.push({
|
|
1584
|
+
target,
|
|
1585
|
+
op: "step",
|
|
1586
|
+
date: args[0],
|
|
1587
|
+
period: args[1],
|
|
1588
|
+
times: args[2],
|
|
1589
|
+
});
|
|
1479
1590
|
else
|
|
1480
1591
|
fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
|
|
1481
1592
|
authored.set(calculations.at(-1), constant);
|
|
@@ -1563,37 +1674,7 @@ export function compile(source, options = {}) {
|
|
|
1563
1674
|
entries: block.entries.flatMap((entry) => {
|
|
1564
1675
|
if (!entry.key.startsWith("when "))
|
|
1565
1676
|
return [entry];
|
|
1566
|
-
const
|
|
1567
|
-
const binding = environment.get(tunable);
|
|
1568
|
-
if (!binding)
|
|
1569
|
-
fail(entry, `unknown branch tunable ${tunable}`, "name an enum or reference tunable declared by this header");
|
|
1570
|
-
let matches;
|
|
1571
|
-
if (relation === "has") {
|
|
1572
|
-
// Inspect declarations, not lowering order: a bound object may
|
|
1573
|
-
// appear after the instrument that asks about its fields.
|
|
1574
|
-
const bound = resolve(binding);
|
|
1575
|
-
const [root, ...children] = text(bound).split(".");
|
|
1576
|
-
const assignment = assignments.get(root);
|
|
1577
|
-
let target = assignment
|
|
1578
|
-
? templates.get(assignment.target)?.body
|
|
1579
|
-
: program.decls
|
|
1580
|
-
.filter((decl) => decl.kind === "instrument")
|
|
1581
|
-
.find((decl) => decl.name === root)?.body;
|
|
1582
|
-
for (const child of children) {
|
|
1583
|
-
const record = target
|
|
1584
|
-
? entries(asBlock(entries(target).get("records"))).get(child)
|
|
1585
|
-
: undefined;
|
|
1586
|
-
target = record ? asBlock(record) : undefined;
|
|
1587
|
-
}
|
|
1588
|
-
if (!target)
|
|
1589
|
-
fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
|
|
1590
|
-
matches = entries(asBlock(entries(target).get("fields"))).has(choice);
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
if (!enums.get(tunable)?.includes(choice))
|
|
1594
|
-
fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
|
|
1595
|
-
matches = text(binding) === choice;
|
|
1596
|
-
}
|
|
1677
|
+
const matches = branchMatches(entry);
|
|
1597
1678
|
const body = asBlock(entry.value);
|
|
1598
1679
|
for (const clause of body.entries)
|
|
1599
1680
|
if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
|
|
@@ -2446,7 +2527,7 @@ export function compile(source, options = {}) {
|
|
|
2446
2527
|
.filter(Boolean)
|
|
2447
2528
|
.join("."),
|
|
2448
2529
|
}
|
|
2449
|
-
: undefined);
|
|
2530
|
+
: undefined, tunables);
|
|
2450
2531
|
}
|
|
2451
2532
|
};
|
|
2452
2533
|
const compileObject = (decl) => {
|
|
@@ -3388,10 +3469,21 @@ export function compile(source, options = {}) {
|
|
|
3388
3469
|
}, "lower");
|
|
3389
3470
|
}),
|
|
3390
3471
|
};
|
|
3472
|
+
const sameParty = samePartyMoves(document, attachedSelectors, attachEntries);
|
|
3473
|
+
const unbound = unboundEconomicSources(document, attachedSelectors, attachEntries);
|
|
3474
|
+
if (sameParty.errors.length || unbound.length)
|
|
3475
|
+
return {
|
|
3476
|
+
verdict: "invalid",
|
|
3477
|
+
warnings: [],
|
|
3478
|
+
diagnostics: [...sameParty.errors, ...unbound].map((d) => diagnostic(d, "check")),
|
|
3479
|
+
};
|
|
3391
3480
|
return {
|
|
3392
3481
|
verdict: "valid",
|
|
3393
3482
|
diagnostics: [],
|
|
3394
|
-
warnings:
|
|
3483
|
+
warnings: [
|
|
3484
|
+
...sameParty.warnings,
|
|
3485
|
+
...purposeWarnings(document, attachedSelectors, attachEntries),
|
|
3486
|
+
].map((d) => ({ ...diagnostic(d, "check"), severity: "warning" })),
|
|
3395
3487
|
artifacts: {
|
|
3396
3488
|
document: validated.value,
|
|
3397
3489
|
costManifest: buildUdlCostManifest(validated.value),
|