@hyperscale0/hsx 5.7.0 → 5.8.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 +18 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +278 -15
- package/dist/src/compile.js.map +1 -1
- package/dist/src/diagnostics.d.ts +2 -0
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +23 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/std-bundle.js +2 -2
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/headers.md +1 -1
- package/package.json +2 -2
- package/src/compile.ts +363 -27
- package/src/diagnostics.ts +34 -0
- package/src/std-bundle.ts +2 -2
- package/src/version.ts +1 -1
- package/std/financing.hsx +3 -1
- package/std/marketplace.hsx +3 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,WAAW,EAAE,2oKAA2oK,CAAC;IAC1pK,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,+qHAA+qH,CAAC;IAC/rH,CAAC,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,WAAW,EAAE,2oKAA2oK,CAAC;IAC1pK,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,+qHAA+qH,CAAC;IAC/rH,CAAC,eAAe,EAAE,6m+DAA6m+D,CAAC;IACho+D,CAAC,eAAe,EAAE,01KAA01K,CAAC;IAC72K,CAAC,aAAa,EAAE,qwTAAqwT,CAAC;IACtxT,CAAC,iBAAiB,EAAE,woTAAwoT,CAAC;IAC7pT,CAAC,WAAW,EAAE,u1VAAu1V,CAAC;IACt2V,CAAC,eAAe,EAAE,649CAA649C,CAAC;IACh69C,CAAC,aAAa,EAAE,85JAA85J,CAAC;IAC/6J,CAAC,YAAY,EAAE,o7SAAo7S,CAAC;IACp8S,CAAC,YAAY,EAAE,2jGAA2jG,CAAC;CAC5kG,CAAC,CAAC"}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HSX_VERSION = "5.
|
|
1
|
+
export declare const HSX_VERSION = "5.8.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const HSX_VERSION = "5.
|
|
1
|
+
export const HSX_VERSION = "5.8.0";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/docs/headers.md
CHANGED
|
@@ -16,7 +16,7 @@ Bind party parameters to owner, actor, operator or declared parties. Configure d
|
|
|
16
16
|
| money.metered | `payer: party`, `payee: party`, `unit_price: money`, `units: integer = 1`, `fee: fee = { seller: 0% }` |
|
|
17
17
|
| marketplace.listing | `seller: party` |
|
|
18
18
|
| marketplace.order | `listing: ref<marketplace.listing>`, `buyer: party` |
|
|
19
|
-
| marketplace.reservation | `listing: ref<marketplace.listing>`, `funds: ref<escrow.hold>`, `
|
|
19
|
+
| marketplace.reservation | `listing: ref<marketplace.listing>`, `funds: ref<escrow.hold>`, `converters: ref[]`, `buyer: party`, `seller: party`, `expires_after: duration = 48h`, `deposit: money = 1000 SAR`, `cancellation_refund: percent = 100%`, `expiry_refund: percent = 100%`, `retained_to: party = programOperator`, `operator: party = programOperator`, `competing_quotes: enum(allowed, blocked) = allowed` |
|
|
20
20
|
| escrow.hold | `payer: party = party(person)`, `payee: party = programOperator`, `accept_within: duration = 48h`, `fee: fee = { seller: 0%, tax: 0% }`, `dispute: policy = { refund_after: return_verified }` |
|
|
21
21
|
| wallet.balance | `holder: party`, `spend_limit: money = 100000 SAR` |
|
|
22
22
|
| wallet.spend | `wallet: ref<wallet.balance>`, `payee: party` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/hsx",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hsx",
|
|
@@ -98,5 +98,5 @@
|
|
|
98
98
|
"Amir Ayub",
|
|
99
99
|
"Sara AlBakaawi"
|
|
100
100
|
],
|
|
101
|
-
"gitHead": "
|
|
101
|
+
"gitHead": "d368f9b62ba1bd8d75e48e77acca613c750ad0b0"
|
|
102
102
|
}
|
package/src/compile.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CompileFailure,
|
|
3
|
+
didYouMean,
|
|
4
|
+
fail,
|
|
5
|
+
failWithCode,
|
|
6
|
+
} from "./diagnostics.ts";
|
|
2
7
|
import { hash as sha256 } from "fast-sha256";
|
|
3
8
|
import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
|
|
4
9
|
import {
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
type UdlField,
|
|
22
27
|
type UdlInstrument,
|
|
23
28
|
type UdlObjectField,
|
|
29
|
+
type UdlSelection,
|
|
24
30
|
type UdlSubjectRequirement,
|
|
25
31
|
type UdlValue,
|
|
26
32
|
} from "@hyperscale0/udl";
|
|
@@ -398,6 +404,18 @@ export function compile(
|
|
|
398
404
|
severity: "error",
|
|
399
405
|
stage,
|
|
400
406
|
});
|
|
407
|
+
// A program instrument lowers once standalone and once per attachment, so
|
|
408
|
+
// one authored mistake can fail each copy at the same place.
|
|
409
|
+
const distinct = (list: Diagnostic[]) =>
|
|
410
|
+
list.filter(
|
|
411
|
+
(d, index) =>
|
|
412
|
+
list.findIndex(
|
|
413
|
+
(other) =>
|
|
414
|
+
other.source === d.source &&
|
|
415
|
+
other.span.start === d.span.start &&
|
|
416
|
+
other.message === d.message,
|
|
417
|
+
) === index,
|
|
418
|
+
);
|
|
401
419
|
if (parsed.diagnostics.length)
|
|
402
420
|
return {
|
|
403
421
|
verdict: "invalid",
|
|
@@ -589,6 +607,10 @@ export function compile(
|
|
|
589
607
|
}
|
|
590
608
|
}
|
|
591
609
|
const origins: CompileOriginMapEntry[] = [];
|
|
610
|
+
// Lowered objects lose their spans, so each keeps the expression that
|
|
611
|
+
// produced it for the name check after lowering.
|
|
612
|
+
const authored = new WeakMap<object, Expr>();
|
|
613
|
+
const declarations = new WeakMap<UdlInstrument, InstrumentDecl>();
|
|
592
614
|
const exposures: { instrument: string; action: string; entry: Entry }[] =
|
|
593
615
|
[];
|
|
594
616
|
const refundBindings = new Map<
|
|
@@ -1179,14 +1201,11 @@ export function compile(
|
|
|
1179
1201
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
1180
1202
|
const type =
|
|
1181
1203
|
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
1182
|
-
|
|
1183
|
-
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
new Set(),
|
|
1188
|
-
!!attachmentInfo && type === "party",
|
|
1189
|
-
);
|
|
1204
|
+
const asWritten =
|
|
1205
|
+
(supplied.has(param.key) && !attachmentInfo) || type === "enum";
|
|
1206
|
+
let v = asWritten
|
|
1207
|
+
? actual
|
|
1208
|
+
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
1190
1209
|
if (
|
|
1191
1210
|
type === "duration" &&
|
|
1192
1211
|
(v.kind === "text" || v.kind === "name") &&
|
|
@@ -1237,7 +1256,16 @@ export function compile(
|
|
|
1237
1256
|
? { role: v.value as SubjectPartyRole }
|
|
1238
1257
|
: { party: v.value };
|
|
1239
1258
|
} else if (type === "ref") {
|
|
1240
|
-
|
|
1259
|
+
// Listed objects resolve like a single binding, so `on: [plan]`
|
|
1260
|
+
// names the sibling attachment that `on: plan` names.
|
|
1261
|
+
const values =
|
|
1262
|
+
v.kind !== "list"
|
|
1263
|
+
? [v]
|
|
1264
|
+
: asWritten
|
|
1265
|
+
? v.items
|
|
1266
|
+
: v.items.map((item) => resolve(item));
|
|
1267
|
+
if (v.kind === "list")
|
|
1268
|
+
environment.set(param.key, { ...v, items: values });
|
|
1241
1269
|
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
1242
1270
|
fail(
|
|
1243
1271
|
v,
|
|
@@ -1587,9 +1615,11 @@ export function compile(
|
|
|
1587
1615
|
};
|
|
1588
1616
|
const val = (expr: Expr): UdlValue => {
|
|
1589
1617
|
const v = resolve(expr);
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1618
|
+
if (v.kind !== "name" || ["true", "false"].includes(v.value))
|
|
1619
|
+
return { literal: literal(v) };
|
|
1620
|
+
const value = { field: path(v) };
|
|
1621
|
+
authored.set(value, expr);
|
|
1622
|
+
return value;
|
|
1593
1623
|
};
|
|
1594
1624
|
const data = (expr: Expr): unknown => {
|
|
1595
1625
|
if (
|
|
@@ -1646,6 +1676,7 @@ export function compile(
|
|
|
1646
1676
|
if (k === "family" || k === "instrument") continue;
|
|
1647
1677
|
result[k] = data(v);
|
|
1648
1678
|
}
|
|
1679
|
+
authored.set(result, value);
|
|
1649
1680
|
return result;
|
|
1650
1681
|
}
|
|
1651
1682
|
|
|
@@ -1674,10 +1705,14 @@ export function compile(
|
|
|
1674
1705
|
};
|
|
1675
1706
|
return null;
|
|
1676
1707
|
}
|
|
1708
|
+
authored.set(result, value);
|
|
1677
1709
|
return result;
|
|
1678
1710
|
}
|
|
1679
|
-
if (value.kind === "list")
|
|
1680
|
-
|
|
1711
|
+
if (value.kind === "list") {
|
|
1712
|
+
const items = value.items.map(data).filter((item) => item !== null);
|
|
1713
|
+
authored.set(items, value);
|
|
1714
|
+
return items;
|
|
1715
|
+
}
|
|
1681
1716
|
if (
|
|
1682
1717
|
value.kind === "call" &&
|
|
1683
1718
|
value.name === "child" &&
|
|
@@ -1921,6 +1956,7 @@ export function compile(
|
|
|
1921
1956
|
`unknown calculation ${constant.name}`,
|
|
1922
1957
|
"use rate, sum, minimum, subtract, multiply, divide, before or after",
|
|
1923
1958
|
);
|
|
1959
|
+
authored.set(calculations.at(-1)!, constant);
|
|
1924
1960
|
} else if (type === "enum" && constant.kind === "name")
|
|
1925
1961
|
f.value = constant.value;
|
|
1926
1962
|
else if (type === "money" && constant.kind === "name")
|
|
@@ -1931,14 +1967,26 @@ export function compile(
|
|
|
1931
1967
|
});
|
|
1932
1968
|
else f.value = literal(constant, String(type));
|
|
1933
1969
|
}
|
|
1970
|
+
authored.set(f, row.value);
|
|
1934
1971
|
result.push(f as UdlField);
|
|
1935
1972
|
}
|
|
1936
1973
|
return result;
|
|
1937
1974
|
};
|
|
1938
1975
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
)
|
|
1976
|
+
const lifecycleBlock = asBlock(body.get("lifecycle"));
|
|
1977
|
+
const lifecycle = data(lifecycleBlock) as UdlInstrument["lifecycle"];
|
|
1978
|
+
const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
|
|
1979
|
+
const checkState = (expr: Expr, state: unknown) => {
|
|
1980
|
+
if (typeof state !== "string" || !states.length) return;
|
|
1981
|
+
if (states.includes(state)) return;
|
|
1982
|
+
fail(
|
|
1983
|
+
expr,
|
|
1984
|
+
`\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`,
|
|
1985
|
+
`Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`,
|
|
1986
|
+
);
|
|
1987
|
+
};
|
|
1988
|
+
const initialExpr = entries(lifecycleBlock).get("initial");
|
|
1989
|
+
if (initialExpr) checkState(initialExpr, lifecycle.initial);
|
|
1942
1990
|
lifecycle.transitions = Object.create(
|
|
1943
1991
|
null,
|
|
1944
1992
|
) as UdlInstrument["lifecycle"]["transitions"];
|
|
@@ -2288,6 +2336,7 @@ export function compile(
|
|
|
2288
2336
|
moves: [],
|
|
2289
2337
|
...(actionSubject ? { subject: actionSubject } : {}),
|
|
2290
2338
|
};
|
|
2339
|
+
authored.set(a, row.value);
|
|
2291
2340
|
for (const binding of boundaryBindings)
|
|
2292
2341
|
if (
|
|
2293
2342
|
!a.subject?.adapters.find((entry) => entry.binding === binding)
|
|
@@ -2309,12 +2358,12 @@ export function compile(
|
|
|
2309
2358
|
`action ${name} needs from and to`,
|
|
2310
2359
|
"declare its source and destination states",
|
|
2311
2360
|
);
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
};
|
|
2361
|
+
const sources = from!.kind === "list" ? from!.items : [from!];
|
|
2362
|
+
const fromStates = sources.map((e) => String(data(e)));
|
|
2363
|
+
const toState = String(data(to!));
|
|
2364
|
+
sources.forEach((e, index) => checkState(e, fromStates[index]));
|
|
2365
|
+
if (toState !== "preserve") checkState(to!, toState);
|
|
2366
|
+
lifecycle.transitions[name] = { from: fromStates, to: toState };
|
|
2318
2367
|
}
|
|
2319
2368
|
for (const [key, expr] of slots) {
|
|
2320
2369
|
if (["from", "to", "input", "summary", "subject"].includes(key))
|
|
@@ -2322,6 +2371,7 @@ export function compile(
|
|
|
2322
2371
|
if (key === "moves") {
|
|
2323
2372
|
const moves = expr.kind === "list" ? expr.items : [expr];
|
|
2324
2373
|
for (const [index, move] of moves.entries()) {
|
|
2374
|
+
const lowered = a.moves.length;
|
|
2325
2375
|
const parts = entries(asBlock(move));
|
|
2326
2376
|
const op = parts.has("operation")
|
|
2327
2377
|
? String(data(parts.get("operation")!))
|
|
@@ -2571,6 +2621,8 @@ export function compile(
|
|
|
2571
2621
|
"split percentages must sum to 100%",
|
|
2572
2622
|
"give the recipients exactly 100% together",
|
|
2573
2623
|
);
|
|
2624
|
+
for (const item of a.moves.slice(lowered))
|
|
2625
|
+
authored.set(item, move);
|
|
2574
2626
|
continue;
|
|
2575
2627
|
}
|
|
2576
2628
|
if (!amount || !from || !to)
|
|
@@ -2767,6 +2819,8 @@ export function compile(
|
|
|
2767
2819
|
`unknown transfer instruction ${op}`,
|
|
2768
2820
|
"choose create, reserve, post, or void from internal_transfer",
|
|
2769
2821
|
);
|
|
2822
|
+
for (const item of a.moves.slice(lowered))
|
|
2823
|
+
authored.set(item, move);
|
|
2770
2824
|
}
|
|
2771
2825
|
} else (a as unknown as Record<string, unknown>)[key] = data(expr);
|
|
2772
2826
|
}
|
|
@@ -2911,6 +2965,7 @@ export function compile(
|
|
|
2911
2965
|
};
|
|
2912
2966
|
}
|
|
2913
2967
|
document.instruments.push(inst);
|
|
2968
|
+
declarations.set(inst, decl);
|
|
2914
2969
|
for (const [key, definition] of records) {
|
|
2915
2970
|
const childAttachment = attachmentInfo
|
|
2916
2971
|
? {
|
|
@@ -3267,7 +3322,7 @@ export function compile(
|
|
|
3267
3322
|
if (bindingDiagnostics.length)
|
|
3268
3323
|
return {
|
|
3269
3324
|
verdict: "invalid",
|
|
3270
|
-
diagnostics: bindingDiagnostics
|
|
3325
|
+
diagnostics: distinct(bindingDiagnostics)
|
|
3271
3326
|
.sort((a, b) => a.span.start - b.span.start)
|
|
3272
3327
|
.map((d) => diagnostic(d, "check")),
|
|
3273
3328
|
};
|
|
@@ -3517,6 +3572,287 @@ export function compile(
|
|
|
3517
3572
|
);
|
|
3518
3573
|
}
|
|
3519
3574
|
}
|
|
3575
|
+
// Names resolve here, once tunables, attachments and derived fields are
|
|
3576
|
+
// final, and each failure points at the expression that wrote the name.
|
|
3577
|
+
type Located = { span: Span; source?: string };
|
|
3578
|
+
type Scope = { inst: UdlInstrument; label: string; action?: UdlAction };
|
|
3579
|
+
const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
|
|
3580
|
+
const site = (owner: object, key: string | number, fallback: Located) => {
|
|
3581
|
+
const expr = authored.get(owner);
|
|
3582
|
+
if (expr?.kind === "block")
|
|
3583
|
+
return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
|
|
3584
|
+
if (expr?.kind === "list" && Array.isArray(owner))
|
|
3585
|
+
return expr.items.length === owner.length
|
|
3586
|
+
? expr.items[Number(key)]!
|
|
3587
|
+
: expr;
|
|
3588
|
+
return expr ?? fallback;
|
|
3589
|
+
};
|
|
3590
|
+
const keySite = (owner: object, key: string, fallback: Located) => {
|
|
3591
|
+
const expr = authored.get(owner);
|
|
3592
|
+
const entry =
|
|
3593
|
+
expr?.kind === "block"
|
|
3594
|
+
? expr.entries.find((item) => item.key === key)
|
|
3595
|
+
: undefined;
|
|
3596
|
+
if (!entry) return fallback;
|
|
3597
|
+
return {
|
|
3598
|
+
...entry,
|
|
3599
|
+
span: { start: entry.span.start, end: entry.span.start + key.length },
|
|
3600
|
+
};
|
|
3601
|
+
};
|
|
3602
|
+
const builtins = (inst: UdlInstrument) => [
|
|
3603
|
+
"id",
|
|
3604
|
+
"now",
|
|
3605
|
+
"createdAt",
|
|
3606
|
+
"productRevision",
|
|
3607
|
+
"status",
|
|
3608
|
+
...(inst.subject ? ["subject"] : []),
|
|
3609
|
+
];
|
|
3610
|
+
const parties = (inst: UdlInstrument) => [
|
|
3611
|
+
...Object.keys(document.parties),
|
|
3612
|
+
...(inst.subject ? subjectPartyRoles : []),
|
|
3613
|
+
];
|
|
3614
|
+
const members = (field: UdlField): string[] => {
|
|
3615
|
+
if (field.type === "account") return ["balance", "reserved"];
|
|
3616
|
+
if (field.type !== "ref") return [];
|
|
3617
|
+
const [id] = [field.target].flat();
|
|
3618
|
+
const target =
|
|
3619
|
+
field.targetKind === "object"
|
|
3620
|
+
? document.objects.find((object) => object.id === id)
|
|
3621
|
+
: byId.get(id!);
|
|
3622
|
+
if (!target) return [];
|
|
3623
|
+
return [
|
|
3624
|
+
...target.fields.map((item) => item.name),
|
|
3625
|
+
...("lifecycle" in target ? builtins(target) : []),
|
|
3626
|
+
];
|
|
3627
|
+
};
|
|
3628
|
+
const checkParty = (name: string, at: Located, inst: UdlInstrument) => {
|
|
3629
|
+
const names = parties(inst);
|
|
3630
|
+
if (!names.includes(name))
|
|
3631
|
+
fail(
|
|
3632
|
+
at,
|
|
3633
|
+
`\`${name}\` is not a declared party.${didYouMean(name, names)}`,
|
|
3634
|
+
`Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
|
|
3635
|
+
);
|
|
3636
|
+
};
|
|
3637
|
+
const checkInput = (
|
|
3638
|
+
name: string,
|
|
3639
|
+
at: Located,
|
|
3640
|
+
label: string,
|
|
3641
|
+
input: readonly UdlField[],
|
|
3642
|
+
) => {
|
|
3643
|
+
const names = input.map((field) => field.name);
|
|
3644
|
+
if (!names.includes(name))
|
|
3645
|
+
fail(
|
|
3646
|
+
at,
|
|
3647
|
+
`\`${name}\` is not an input of \`${label}\`.${didYouMean(name, names)}`,
|
|
3648
|
+
`Declare \`${name}\` in the input of \`${label}\`${names.length ? `, or use one of ${names.join(", ")}` : ""}.`,
|
|
3649
|
+
);
|
|
3650
|
+
};
|
|
3651
|
+
const checkTarget = (id: string, at: Located, objects = false) => {
|
|
3652
|
+
const ids = [
|
|
3653
|
+
...byId.keys(),
|
|
3654
|
+
...(objects ? document.objects.map((object) => object.id) : []),
|
|
3655
|
+
];
|
|
3656
|
+
const noun = objects ? "instrument or object" : "instrument";
|
|
3657
|
+
if (!ids.includes(id))
|
|
3658
|
+
fail(
|
|
3659
|
+
at,
|
|
3660
|
+
`\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`,
|
|
3661
|
+
`Name a declared ${noun}.`,
|
|
3662
|
+
);
|
|
3663
|
+
};
|
|
3664
|
+
const checkPath = (path: string, at: Located, scope: Scope): void => {
|
|
3665
|
+
const { inst, action } = scope;
|
|
3666
|
+
const parts = path.split(".");
|
|
3667
|
+
const found = (count: number) =>
|
|
3668
|
+
resolveField(
|
|
3669
|
+
document,
|
|
3670
|
+
inst,
|
|
3671
|
+
parts.slice(0, count).join("."),
|
|
3672
|
+
action?.input,
|
|
3673
|
+
action,
|
|
3674
|
+
);
|
|
3675
|
+
// Subject paths keep their own check against the action's requirements.
|
|
3676
|
+
if (found(parts.length) || parts[0] === "subject") return;
|
|
3677
|
+
if (parts[0] === "party" && parts[1]) {
|
|
3678
|
+
checkParty(parts[1], at, inst);
|
|
3679
|
+
if (!found(2)) return;
|
|
3680
|
+
}
|
|
3681
|
+
if (!["self", "input", "party"].includes(parts[0]!))
|
|
3682
|
+
parts.unshift("self");
|
|
3683
|
+
let count = 2;
|
|
3684
|
+
while (count <= parts.length && found(count)) count++;
|
|
3685
|
+
if (count > parts.length) return;
|
|
3686
|
+
const name = parts[count - 1]!;
|
|
3687
|
+
if (count > 2) {
|
|
3688
|
+
const prefix = parts.slice(0, count - 1).join(".");
|
|
3689
|
+
const names = members(found(count - 1)!);
|
|
3690
|
+
fail(
|
|
3691
|
+
at,
|
|
3692
|
+
`\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`,
|
|
3693
|
+
names.length
|
|
3694
|
+
? `Use one of ${names.join(", ")}.`
|
|
3695
|
+
: `\`${prefix}\` has no fields.`,
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
if (parts[0] === "input")
|
|
3699
|
+
return checkInput(name, at, scope.label, action?.input ?? []);
|
|
3700
|
+
const declared = declarations.get(inst)!.name;
|
|
3701
|
+
const fields = inst.fields.map((field) => field.name);
|
|
3702
|
+
fail(
|
|
3703
|
+
at,
|
|
3704
|
+
`\`${name}\` is not a field or party of \`${declared}\`.${didYouMean(name, [...fields, ...builtins(inst), ...parties(inst)])}`,
|
|
3705
|
+
`Use a field declared in \`${declared}\` (${fields.join(", ")}) or a declared party (${parties(inst).join(", ")}).`,
|
|
3706
|
+
);
|
|
3707
|
+
};
|
|
3708
|
+
const scopeOf = (inst: UdlInstrument): Scope => ({
|
|
3709
|
+
inst,
|
|
3710
|
+
label: declarations.get(inst)!.name,
|
|
3711
|
+
});
|
|
3712
|
+
const checkSelection = (
|
|
3713
|
+
selection: UdlSelection,
|
|
3714
|
+
at: Located,
|
|
3715
|
+
scope: Scope,
|
|
3716
|
+
) => {
|
|
3717
|
+
walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
|
|
3718
|
+
walk(selection.where, "where", site(selection, "where", at), scope);
|
|
3719
|
+
for (const id of [selection.instrument].flat()) {
|
|
3720
|
+
checkTarget(id, site(selection, "instrument", at));
|
|
3721
|
+
const target = scopeOf(byId.get(id)!);
|
|
3722
|
+
const member = (key: string, where: Located) =>
|
|
3723
|
+
checkPath(
|
|
3724
|
+
key.startsWith("self.") ? key : `self.${key}`,
|
|
3725
|
+
where,
|
|
3726
|
+
target,
|
|
3727
|
+
);
|
|
3728
|
+
for (const key of Object.keys(selection.where ?? {}))
|
|
3729
|
+
member(key, keySite(selection.where!, key, at));
|
|
3730
|
+
member(selection.reference, site(selection, "reference", at));
|
|
3731
|
+
selection.order?.forEach((key, index) =>
|
|
3732
|
+
member(key, site(selection.order!, index, at)),
|
|
3733
|
+
);
|
|
3734
|
+
if (selection.window)
|
|
3735
|
+
member(selection.window.field, site(selection.window, "field", at));
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
const checkCall = (
|
|
3739
|
+
call: NonNullable<UdlAction["invoke"]>[number],
|
|
3740
|
+
at: Located,
|
|
3741
|
+
scope: Scope,
|
|
3742
|
+
) => {
|
|
3743
|
+
const bind = "range" in call ? call.range?.bind : undefined;
|
|
3744
|
+
for (const [key, value] of Object.entries(call))
|
|
3745
|
+
if (key !== "input") walk(value, key, site(call, key, at), scope);
|
|
3746
|
+
for (const [key, value] of Object.entries(call.input))
|
|
3747
|
+
if (!("field" in value) || value.field !== bind)
|
|
3748
|
+
walk(value, key, site(call.input, key, at), scope);
|
|
3749
|
+
if ("instrument" in call)
|
|
3750
|
+
checkTarget(call.instrument, site(call, "instrument", at));
|
|
3751
|
+
const reference =
|
|
3752
|
+
"reference" in call
|
|
3753
|
+
? resolveField(
|
|
3754
|
+
document,
|
|
3755
|
+
scope.inst,
|
|
3756
|
+
call.reference,
|
|
3757
|
+
scope.action?.input,
|
|
3758
|
+
scope.action,
|
|
3759
|
+
)
|
|
3760
|
+
: undefined;
|
|
3761
|
+
const ids =
|
|
3762
|
+
"instrument" in call
|
|
3763
|
+
? [call.instrument]
|
|
3764
|
+
: "selection" in call
|
|
3765
|
+
? [call.selection.instrument].flat()
|
|
3766
|
+
: reference?.type === "ref" && reference.targetKind === "instrument"
|
|
3767
|
+
? [reference.target].flat()
|
|
3768
|
+
: [];
|
|
3769
|
+
for (const id of ids) {
|
|
3770
|
+
const target = byId.get(id);
|
|
3771
|
+
if (!target) continue;
|
|
3772
|
+
const action = target.actions[call.action];
|
|
3773
|
+
if (!action)
|
|
3774
|
+
fail(
|
|
3775
|
+
site(call, "action", at),
|
|
3776
|
+
`\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`,
|
|
3777
|
+
`Use one of ${target.actionOrder.join(", ")}.`,
|
|
3778
|
+
);
|
|
3779
|
+
for (const key of Object.keys(call.input))
|
|
3780
|
+
checkInput(
|
|
3781
|
+
key,
|
|
3782
|
+
keySite(call.input, key, at),
|
|
3783
|
+
`${id}.${call.action}`,
|
|
3784
|
+
action.input,
|
|
3785
|
+
);
|
|
3786
|
+
}
|
|
3787
|
+
};
|
|
3788
|
+
const pathKeys = [
|
|
3789
|
+
"field",
|
|
3790
|
+
"reference",
|
|
3791
|
+
"anchor",
|
|
3792
|
+
"at",
|
|
3793
|
+
"list",
|
|
3794
|
+
"from",
|
|
3795
|
+
"to",
|
|
3796
|
+
"transfer",
|
|
3797
|
+
"subject",
|
|
3798
|
+
"instruction",
|
|
3799
|
+
"installment",
|
|
3800
|
+
"recipient",
|
|
3801
|
+
"dueAt",
|
|
3802
|
+
"fields",
|
|
3803
|
+
];
|
|
3804
|
+
const walk = (
|
|
3805
|
+
value: unknown,
|
|
3806
|
+
key: string,
|
|
3807
|
+
at: Located,
|
|
3808
|
+
scope: Scope,
|
|
3809
|
+
): void => {
|
|
3810
|
+
if (typeof value === "string") {
|
|
3811
|
+
if (key === "party") checkParty(value, at, scope.inst);
|
|
3812
|
+
if (key === "target") checkPath(`self.${value}`, at, scope);
|
|
3813
|
+
if (pathKeys.includes(key)) checkPath(value, at, scope);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
if (!value || typeof value !== "object") return;
|
|
3817
|
+
if (Array.isArray(value))
|
|
3818
|
+
return value.forEach((item, index) =>
|
|
3819
|
+
walk(item, key, site(value, index, at), scope),
|
|
3820
|
+
);
|
|
3821
|
+
if (key === "selection")
|
|
3822
|
+
return checkSelection(value as UdlSelection, at, scope);
|
|
3823
|
+
if (key === "invoke")
|
|
3824
|
+
return checkCall(
|
|
3825
|
+
value as NonNullable<UdlAction["invoke"]>[number],
|
|
3826
|
+
at,
|
|
3827
|
+
scope,
|
|
3828
|
+
);
|
|
3829
|
+
if (key === "set")
|
|
3830
|
+
for (const name of Object.keys(value))
|
|
3831
|
+
checkPath(`self.${name}`, keySite(value, name, at), scope);
|
|
3832
|
+
for (const [child, item] of Object.entries(value))
|
|
3833
|
+
walk(item, child, site(value, child, at), scope);
|
|
3834
|
+
};
|
|
3835
|
+
for (const inst of document.instruments) {
|
|
3836
|
+
const decl = declarations.get(inst)!;
|
|
3837
|
+
const scope = scopeOf(inst);
|
|
3838
|
+
for (const field of [
|
|
3839
|
+
...inst.fields,
|
|
3840
|
+
...Object.values(inst.actions).flatMap((action) => action.input),
|
|
3841
|
+
])
|
|
3842
|
+
if ("target" in field && field.target !== undefined)
|
|
3843
|
+
for (const id of [field.target].flat())
|
|
3844
|
+
checkTarget(id, site(field, "target", decl), true);
|
|
3845
|
+
walk(inst.calculate, "calculate", decl, scope);
|
|
3846
|
+
walk(inst.invariants, "invariants", decl, scope);
|
|
3847
|
+
for (const [name, action] of Object.entries(inst.actions))
|
|
3848
|
+
for (const [key, value] of Object.entries(action))
|
|
3849
|
+
if (key !== "input" && key !== "subject")
|
|
3850
|
+
walk(value, key, site(action, key, decl), {
|
|
3851
|
+
inst,
|
|
3852
|
+
label: `${decl.name}.${name}`,
|
|
3853
|
+
action,
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3520
3856
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
3521
3857
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
3522
3858
|
for (const inst of document.instruments) {
|
|
@@ -3754,8 +4090,8 @@ export function compile(
|
|
|
3754
4090
|
if (error instanceof CompileFailure)
|
|
3755
4091
|
return {
|
|
3756
4092
|
verdict: "invalid",
|
|
3757
|
-
diagnostics: [...bindingDiagnostics, error.diagnostic].map(
|
|
3758
|
-
diagnostic(d, "check"),
|
|
4093
|
+
diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map(
|
|
4094
|
+
(d) => diagnostic(d, "check"),
|
|
3759
4095
|
),
|
|
3760
4096
|
};
|
|
3761
4097
|
throw error;
|
package/src/diagnostics.ts
CHANGED
|
@@ -26,3 +26,37 @@ export function failWithCode(
|
|
|
26
26
|
...(expr.source ? { source: expr.source } : {}),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
// Levenshtein distance where swapping two adjacent letters counts as one edit.
|
|
31
|
+
function editDistance(a: string, b: string): number {
|
|
32
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
33
|
+
Array.from({ length: b.length + 1 }, (_, j) =>
|
|
34
|
+
i === 0 ? j : j === 0 ? i : 0,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
for (let i = 1; i <= a.length; i++)
|
|
38
|
+
for (let j = 1; j <= b.length; j++) {
|
|
39
|
+
d[i]![j] = Math.min(
|
|
40
|
+
d[i - 1]![j]! + 1,
|
|
41
|
+
d[i]![j - 1]! + 1,
|
|
42
|
+
d[i - 1]![j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
43
|
+
);
|
|
44
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
45
|
+
d[i]![j] = Math.min(d[i]![j]!, d[i - 2]![j - 2]! + 1);
|
|
46
|
+
}
|
|
47
|
+
return d[a.length]![b.length]!;
|
|
48
|
+
}
|
|
49
|
+
/** " Did you mean `x`?" for the closest candidate within a third of the name's length, else "". */
|
|
50
|
+
export function didYouMean(
|
|
51
|
+
name: string,
|
|
52
|
+
candidates: readonly string[],
|
|
53
|
+
): string {
|
|
54
|
+
const limit = Math.max(1, Math.floor(name.length / 3));
|
|
55
|
+
let best: string | undefined;
|
|
56
|
+
let bestDistance = limit + 1;
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
const distance = editDistance(name, candidate);
|
|
59
|
+
if (distance < bestDistance) [best, bestDistance] = [candidate, distance];
|
|
60
|
+
}
|
|
61
|
+
return best === undefined ? "" : ` Did you mean \`${best}\`?`;
|
|
62
|
+
}
|