@venn-lang/core 0.1.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/index.d.ts +3654 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13137 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/ast/call-args.ts +15 -0
- package/src/ast/dotted-path.ts +22 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/is-statement.ts +40 -0
- package/src/ast/split-call.ts +28 -0
- package/src/ast/walk.ts +6 -0
- package/src/codes/build-problem.ts +24 -0
- package/src/codes/catalog.ts +35 -0
- package/src/codes/code.types.ts +7 -0
- package/src/codes/index.ts +3 -0
- package/src/compile/compile.ts +169 -0
- package/src/compile/compile.types.ts +40 -0
- package/src/compile/index.ts +3 -0
- package/src/compile/lex-scope.ts +78 -0
- package/src/compile/nodes/binary.ts +56 -0
- package/src/compile/nodes/call.ts +50 -0
- package/src/compile/nodes/collection.ts +83 -0
- package/src/compile/nodes/const-lit.ts +30 -0
- package/src/compile/nodes/fast-binary.ts +71 -0
- package/src/compile/nodes/fn.ts +87 -0
- package/src/compile/nodes/index.ts +8 -0
- package/src/compile/nodes/literal.ts +86 -0
- package/src/compile/nodes/map-fields.ts +79 -0
- package/src/compile/nodes/member.ts +32 -0
- package/src/events/envelope.types.ts +17 -0
- package/src/events/event-data.types.ts +25 -0
- package/src/events/ids.types.ts +10 -0
- package/src/events/index.ts +5 -0
- package/src/events/run-plan.types.ts +18 -0
- package/src/events/status.types.ts +2 -0
- package/src/expand/accepted-kinds.ts +23 -0
- package/src/expand/deco/deco-decorator.ts +77 -0
- package/src/expand/deco/deco-env.ts +24 -0
- package/src/expand/deco/deco.types.ts +43 -0
- package/src/expand/deco/document-decos.ts +64 -0
- package/src/expand/deco/index.ts +7 -0
- package/src/expand/deco/read-signature.ts +57 -0
- package/src/expand/deco/run-body.ts +119 -0
- package/src/expand/decorate-callable.ts +49 -0
- package/src/expand/decorations.ts +41 -0
- package/src/expand/expand.ts +108 -0
- package/src/expand/expand.types.ts +78 -0
- package/src/expand/handles/around-verbs.ts +35 -0
- package/src/expand/handles/binding-verbs.ts +27 -0
- package/src/expand/handles/common-verbs.ts +40 -0
- package/src/expand/handles/fn-verbs.ts +58 -0
- package/src/expand/handles/handle.types.ts +33 -0
- package/src/expand/handles/index.ts +4 -0
- package/src/expand/handles/kind-of.ts +36 -0
- package/src/expand/handles/make-handle.ts +100 -0
- package/src/expand/handles/missing-verb.ts +27 -0
- package/src/expand/handles/runnable-verbs.ts +13 -0
- package/src/expand/handles/type-verbs.ts +56 -0
- package/src/expand/index.ts +40 -0
- package/src/expand/make-context.ts +69 -0
- package/src/expand/node-meta.ts +30 -0
- package/src/expand/node-span.ts +17 -0
- package/src/expand/swap-node.ts +28 -0
- package/src/expand/wrong-kind.ts +102 -0
- package/src/expr/cell.types.ts +25 -0
- package/src/expr/closure.ts +53 -0
- package/src/expr/closure.types.ts +31 -0
- package/src/expr/eval-env.types.ts +9 -0
- package/src/expr/evaluate.ts +18 -0
- package/src/expr/frame.ts +60 -0
- package/src/expr/index.ts +14 -0
- package/src/expr/invoke.ts +140 -0
- package/src/expr/member-value.ts +91 -0
- package/src/expr/methods/index.ts +65 -0
- package/src/expr/methods/list-grouping.ts +107 -0
- package/src/expr/methods/list-methods.ts +57 -0
- package/src/expr/methods/list-selection.ts +142 -0
- package/src/expr/methods/map-extras.ts +87 -0
- package/src/expr/methods/map-methods.ts +17 -0
- package/src/expr/methods/number-methods.ts +29 -0
- package/src/expr/methods/string-extras.ts +66 -0
- package/src/expr/methods/string-methods.ts +32 -0
- package/src/expr/methods/unit-methods.ts +54 -0
- package/src/expr/namespace.ts +20 -0
- package/src/expr/native.types.ts +40 -0
- package/src/expr/operators.ts +91 -0
- package/src/expr/pending.ts +47 -0
- package/src/expr/prelude.ts +75 -0
- package/src/expr/task.ts +71 -0
- package/src/format/format-text.ts +22 -0
- package/src/format/format.types.ts +18 -0
- package/src/format/from-settings.ts +40 -0
- package/src/format/index.ts +5 -0
- package/src/format/organize-header.ts +66 -0
- package/src/format/reindent.ts +35 -0
- package/src/generated/ast.ts +2383 -0
- package/src/generated/grammar.ts +5236 -0
- package/src/generated/module.ts +25 -0
- package/src/grammar/venn.langium +291 -0
- package/src/graph/graph.types.ts +20 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/to-graph.ts +87 -0
- package/src/index.ts +93 -0
- package/src/interpolation/compile-template.ts +40 -0
- package/src/interpolation/index.ts +4 -0
- package/src/interpolation/interpolation.types.ts +18 -0
- package/src/interpolation/scan-interpolations.ts +47 -0
- package/src/interpolation/template.types.ts +19 -0
- package/src/lang/default-services.ts +14 -0
- package/src/lang/index.ts +3 -0
- package/src/lang/venn-lexer.ts +34 -0
- package/src/lang/venn-module.ts +51 -0
- package/src/module/index.ts +5 -0
- package/src/module/specifier.ts +21 -0
- package/src/parse/error-to-problem.ts +43 -0
- package/src/parse/index.ts +3 -0
- package/src/parse/parse-expression.ts +27 -0
- package/src/parse/parse-output.types.ts +8 -0
- package/src/parse/parse.ts +26 -0
- package/src/problem/build-diff.ts +116 -0
- package/src/problem/diff.types.ts +20 -0
- package/src/problem/format-value.ts +40 -0
- package/src/problem/index.ts +8 -0
- package/src/problem/problem-error.ts +15 -0
- package/src/problem/problem.types.ts +21 -0
- package/src/problem/related.types.ts +7 -0
- package/src/problem/severity.types.ts +2 -0
- package/src/problem/span.types.ts +8 -0
- package/src/typecheck/action-signature.ts +53 -0
- package/src/typecheck/builtin-types.ts +63 -0
- package/src/typecheck/builtins.ts +267 -0
- package/src/typecheck/catalog.types.ts +16 -0
- package/src/typecheck/check-deco.ts +174 -0
- package/src/typecheck/check-stmts.ts +151 -0
- package/src/typecheck/check-types.ts +232 -0
- package/src/typecheck/context.ts +31 -0
- package/src/typecheck/imported-types.ts +127 -0
- package/src/typecheck/index.ts +26 -0
- package/src/typecheck/infer.ts +416 -0
- package/src/typecheck/kind-types.ts +79 -0
- package/src/typecheck/member-docs.ts +211 -0
- package/src/typecheck/named-types.ts +57 -0
- package/src/typecheck/prelude-types.ts +152 -0
- package/src/typecheck/reshaped-fns.ts +58 -0
- package/src/typecheck/scheme.ts +75 -0
- package/src/typecheck/seed-params.ts +72 -0
- package/src/typecheck/seed-values.ts +64 -0
- package/src/typecheck/show.ts +64 -0
- package/src/typecheck/solidify.ts +60 -0
- package/src/typecheck/spec-to-type.ts +88 -0
- package/src/typecheck/type-env.ts +46 -0
- package/src/typecheck/type-ref.ts +62 -0
- package/src/typecheck/type.types.ts +168 -0
- package/src/typecheck/unify.ts +192 -0
- package/src/typecheck/unit-types.ts +49 -0
- package/src/units/combine.ts +97 -0
- package/src/units/index.ts +6 -0
- package/src/units/parse-instant.ts +12 -0
- package/src/units/parse-number.ts +48 -0
- package/src/units/unit-guards.ts +16 -0
- package/src/units/unit.types.ts +38 -0
- package/src/value/equals.ts +4 -0
- package/src/value/index.ts +4 -0
- package/src/value/is-numeric.ts +6 -0
- package/src/value/truthiness.ts +9 -0
- package/src/value/value.types.ts +12 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { truthy } from "../../value/index.js";
|
|
2
|
+
import { type Invoke, type Method, nativeFn } from "../native.types.js";
|
|
3
|
+
|
|
4
|
+
const num = (value: unknown): number => Number(value);
|
|
5
|
+
|
|
6
|
+
/** Selecting, ordering and summarising: the other half of everyday list work. */
|
|
7
|
+
export const LIST_SELECTION: Record<string, Method> = {
|
|
8
|
+
take: (list: readonly unknown[]) => nativeFn((args) => list.slice(0, count(args[0]))),
|
|
9
|
+
drop: (list: readonly unknown[]) => nativeFn((args) => list.slice(count(args[0]))),
|
|
10
|
+
// Counted from the front, because `slice(-0)` is `slice(0)` and would hand
|
|
11
|
+
// back the whole list when asked for none of it.
|
|
12
|
+
takeLast: (list: readonly unknown[]) =>
|
|
13
|
+
nativeFn((args) => list.slice(Math.max(0, list.length - count(args[0])))),
|
|
14
|
+
dropLast: (list: readonly unknown[]) =>
|
|
15
|
+
nativeFn((args) => list.slice(0, Math.max(0, list.length - count(args[0])))),
|
|
16
|
+
takeWhile: (list: readonly unknown[], invoke: Invoke) =>
|
|
17
|
+
nativeFn((args) => list.slice(0, edge(list, args[0], invoke))),
|
|
18
|
+
dropWhile: (list: readonly unknown[], invoke: Invoke) =>
|
|
19
|
+
nativeFn((args) => list.slice(edge(list, args[0], invoke))),
|
|
20
|
+
distinct: (list: readonly unknown[]) => distinct(list, (item) => item),
|
|
21
|
+
distinctBy: (list: readonly unknown[], invoke: Invoke) =>
|
|
22
|
+
nativeFn((args) => distinct(list, (item, i) => invoke.two(args[0], item, i))),
|
|
23
|
+
sortBy: (list: readonly unknown[], invoke: Invoke) =>
|
|
24
|
+
nativeFn((args) => sortBy(list, (item, i) => invoke.two(args[0], item, i))),
|
|
25
|
+
minBy: (list: readonly unknown[], invoke: Invoke) =>
|
|
26
|
+
nativeFn((args) => best(list, (item, i) => num(invoke.two(args[0], item, i)), -1)),
|
|
27
|
+
maxBy: (list: readonly unknown[], invoke: Invoke) =>
|
|
28
|
+
nativeFn((args) => best(list, (item, i) => num(invoke.two(args[0], item, i)), 1)),
|
|
29
|
+
sumBy: (list: readonly unknown[], invoke: Invoke) =>
|
|
30
|
+
nativeFn((args) =>
|
|
31
|
+
list.reduce((total: number, x, i) => total + num(invoke.two(args[0], x, i)), 0),
|
|
32
|
+
),
|
|
33
|
+
flatMap: (list: readonly unknown[], invoke: Invoke) =>
|
|
34
|
+
nativeFn((args) => list.flatMap((item, i) => flatten(invoke.two(args[0], item, i)))),
|
|
35
|
+
sum: (list: readonly unknown[]) => list.reduce((total: number, x) => total + num(x), 0),
|
|
36
|
+
average: (list: readonly unknown[]) =>
|
|
37
|
+
list.length === 0 ? 0 : list.reduce((total: number, x) => total + num(x), 0) / list.length,
|
|
38
|
+
// Folded rather than spread: `Math.min(...list)` passes every element as an
|
|
39
|
+
// argument, and a list of a few hundred thousand overflows the call stack.
|
|
40
|
+
min: (list: readonly unknown[]) => extreme(list, -1),
|
|
41
|
+
max: (list: readonly unknown[]) => extreme(list, 1),
|
|
42
|
+
/** The inverse of a map's `entries`: `[["a", 1]].toMap` gives `{ a: 1 }`. */
|
|
43
|
+
toMap: (list: readonly unknown[]) => toMap(list),
|
|
44
|
+
isEmpty: (list: readonly unknown[]) => list.length === 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function toMap(list: readonly unknown[]): Record<string, unknown> {
|
|
48
|
+
const out: Record<string, unknown> = {};
|
|
49
|
+
for (const entry of list) {
|
|
50
|
+
const pair = Array.isArray(entry) ? entry : [];
|
|
51
|
+
if (pair.length > 0) out[String(pair[0])] = pair[1];
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function count(value: unknown): number {
|
|
57
|
+
return Math.max(0, Math.trunc(Number(value ?? 0)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The smallest or largest number in a list, or null when there is none. */
|
|
61
|
+
function extreme(list: readonly unknown[], direction: 1 | -1): number | null {
|
|
62
|
+
if (list.length === 0) return null;
|
|
63
|
+
let best = num(list[0]);
|
|
64
|
+
for (let at = 1; at < list.length; at += 1) {
|
|
65
|
+
const value = num(list[at]);
|
|
66
|
+
if (Number.isNaN(value)) return Number.NaN;
|
|
67
|
+
if (value * direction > best * direction) best = value;
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Where the run of items satisfying the predicate ends. */
|
|
73
|
+
function edge(list: readonly unknown[], fn: unknown, invoke: Invoke): number {
|
|
74
|
+
let index = 0;
|
|
75
|
+
while (index < list.length && truthy(invoke.two(fn, list[index], index))) index += 1;
|
|
76
|
+
return index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function distinct(
|
|
80
|
+
list: readonly unknown[],
|
|
81
|
+
keyOf: (item: unknown, i: number) => unknown,
|
|
82
|
+
): unknown[] {
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
const out: unknown[] = [];
|
|
85
|
+
list.forEach((item, index) => {
|
|
86
|
+
const key = JSON.stringify(keyOf(item, index)) ?? "";
|
|
87
|
+
if (seen.has(key)) return;
|
|
88
|
+
seen.add(key);
|
|
89
|
+
out.push(item);
|
|
90
|
+
});
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sort by a derived key, ordering positions rather than the items themselves.
|
|
96
|
+
*
|
|
97
|
+
* The keys go in one array and the positions in another, so the sort moves
|
|
98
|
+
* numbers and nothing is allocated per item. It stays stable, which is what
|
|
99
|
+
* leaves items whose keys tie in the order they came.
|
|
100
|
+
*/
|
|
101
|
+
function sortBy(list: readonly unknown[], keyOf: (item: unknown, i: number) => unknown): unknown[] {
|
|
102
|
+
const size = list.length;
|
|
103
|
+
const keys = new Array<unknown>(size);
|
|
104
|
+
const order = new Array<number>(size);
|
|
105
|
+
for (let at = 0; at < size; at += 1) {
|
|
106
|
+
keys[at] = keyOf(list[at], at);
|
|
107
|
+
order[at] = at;
|
|
108
|
+
}
|
|
109
|
+
order.sort((left, right) => compare(keys[left], keys[right]));
|
|
110
|
+
const out = new Array<unknown>(size);
|
|
111
|
+
for (let at = 0; at < size; at += 1) out[at] = list[order[at] as number];
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Numbers compare as numbers; anything else compares as its text. */
|
|
116
|
+
function compare(left: unknown, right: unknown): number {
|
|
117
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
118
|
+
const a = String(left);
|
|
119
|
+
const b = String(right);
|
|
120
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function best(
|
|
124
|
+
list: readonly unknown[],
|
|
125
|
+
scoreOf: (item: unknown, i: number) => number,
|
|
126
|
+
direction: number,
|
|
127
|
+
): unknown {
|
|
128
|
+
let winner: unknown = null;
|
|
129
|
+
let score = Number.NaN;
|
|
130
|
+
list.forEach((item, index) => {
|
|
131
|
+
const value = scoreOf(item, index);
|
|
132
|
+
if (Number.isNaN(score) || (value - score) * direction > 0) {
|
|
133
|
+
winner = item;
|
|
134
|
+
score = value;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return winner;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function flatten(value: unknown): unknown[] {
|
|
141
|
+
return Array.isArray(value) ? value : [value];
|
|
142
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type Invoke, type Method, nativeFn } from "../native.types.js";
|
|
2
|
+
|
|
3
|
+
type Dict = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
/** Reshaping a map without a loop: rename, project, combine, walk a path. */
|
|
6
|
+
export const MAP_EXTRAS: Record<string, Method> = {
|
|
7
|
+
mapValues: (map: Dict, invoke: Invoke) =>
|
|
8
|
+
nativeFn((args) => rebuild(map, ([k, v]) => [k, invoke.two(args[0], v, k)])),
|
|
9
|
+
mapKeys: (map: Dict, invoke: Invoke) =>
|
|
10
|
+
nativeFn((args) => rebuild(map, ([k, v]) => [String(invoke.two(args[0], k, v)), v])),
|
|
11
|
+
filterValues: (map: Dict, invoke: Invoke) =>
|
|
12
|
+
nativeFn((args) => keepIf(map, ([k, v]) => Boolean(invoke.two(args[0], v, k)))),
|
|
13
|
+
pick: (map: Dict) => nativeFn((args) => keepIf(map, ([k]) => names(args).includes(k))),
|
|
14
|
+
omit: (map: Dict) => nativeFn((args) => keepIf(map, ([k]) => !names(args).includes(k))),
|
|
15
|
+
mergeDeep: (map: Dict) => nativeFn((args) => mergeDeep(map, asDict(args[0]))),
|
|
16
|
+
invert: (map: Dict) => rebuild(map, ([k, v]) => [String(v), k]),
|
|
17
|
+
isEmpty: (map: Dict) => Object.keys(map).length === 0,
|
|
18
|
+
/** `getPath("a.b.c")` reaches into nested data without a chain of guards. */
|
|
19
|
+
getPath: (map: Dict) => nativeFn((args) => walk(map, String(args[0] ?? ""))),
|
|
20
|
+
/** Whether the path leads anywhere, including to a field holding `null`. */
|
|
21
|
+
hasPath: (map: Dict) => nativeFn((args) => reach(map, String(args[0] ?? "")) !== ABSENT),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** The inverse of `entries`: `fromEntries([["a", 1]])` gives `{ a: 1 }`. */
|
|
25
|
+
export function fromEntries(list: readonly unknown[]): Dict {
|
|
26
|
+
const out: Dict = {};
|
|
27
|
+
for (const entry of list) {
|
|
28
|
+
const pair = Array.isArray(entry) ? entry : [];
|
|
29
|
+
if (pair.length > 0) out[String(pair[0])] = pair[1];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rebuild(map: Dict, step: (entry: [string, unknown]) => [string, unknown]): Dict {
|
|
35
|
+
return Object.fromEntries(Object.entries(map).map(step));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function keepIf(map: Dict, keep: (entry: [string, unknown]) => boolean): Dict {
|
|
39
|
+
return Object.fromEntries(Object.entries(map).filter(keep));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Flatten the argument list: `pick("a", "b")` and `pick(["a", "b"])` both work. */
|
|
43
|
+
function names(args: readonly unknown[]): string[] {
|
|
44
|
+
return args.flatMap((arg) => (Array.isArray(arg) ? arg : [arg])).map(String);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeDeep(left: Dict, right: Dict): Dict {
|
|
48
|
+
const out: Dict = { ...left };
|
|
49
|
+
for (const [key, value] of Object.entries(right)) {
|
|
50
|
+
const current = out[key];
|
|
51
|
+
out[key] = isDict(current) && isDict(value) ? mergeDeep(current, value) : value;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Nothing at all is here, told apart from a field that holds nothing. */
|
|
57
|
+
const ABSENT = Symbol("venn.absent");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Follow a dotted path, answering {@link ABSENT} when it leads nowhere.
|
|
61
|
+
*
|
|
62
|
+
* A field holding `null` and a field that does not exist are different facts,
|
|
63
|
+
* and collapsing them made `hasPath` answer `false` for a key that was plainly
|
|
64
|
+
* there.
|
|
65
|
+
*/
|
|
66
|
+
function reach(map: Dict, path: string): unknown {
|
|
67
|
+
let current: unknown = map;
|
|
68
|
+
for (const part of path.split(".")) {
|
|
69
|
+
if (!isDict(current) && !Array.isArray(current)) return ABSENT;
|
|
70
|
+
if (!Object.hasOwn(current as Dict, part)) return ABSENT;
|
|
71
|
+
current = (current as Dict)[part];
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function walk(map: Dict, path: string): unknown {
|
|
77
|
+
const found = reach(map, path);
|
|
78
|
+
return found === ABSENT ? null : (found ?? null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDict(value: unknown): value is Dict {
|
|
82
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function asDict(value: unknown): Dict {
|
|
86
|
+
return isDict(value) ? value : {};
|
|
87
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Method, nativeFn } from "../native.types.js";
|
|
2
|
+
|
|
3
|
+
type Dict = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Methods on a map value. Checked only after a data property of the same name,
|
|
7
|
+
* so a map with a key called `keys` still reads that key.
|
|
8
|
+
*/
|
|
9
|
+
export const MAP_METHODS: Record<string, Method> = {
|
|
10
|
+
keys: (map: Dict) => Object.keys(map),
|
|
11
|
+
values: (map: Dict) => Object.values(map),
|
|
12
|
+
entries: (map: Dict) => Object.entries(map).map(([key, value]) => [key, value]),
|
|
13
|
+
has: (map: Dict) => nativeFn((args) => String(args[0]) in map),
|
|
14
|
+
get: (map: Dict) => nativeFn((args) => map[String(args[0])] ?? null),
|
|
15
|
+
len: (map: Dict) => Object.keys(map).length,
|
|
16
|
+
merge: (map: Dict) => nativeFn((args) => ({ ...map, ...(args[0] as Dict) })),
|
|
17
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Method, nativeFn } from "../native.types.js";
|
|
2
|
+
|
|
3
|
+
/** Methods on a number. Rounding and clamping without reaching for a namespace. */
|
|
4
|
+
export const NUMBER_METHODS: Record<string, Method> = {
|
|
5
|
+
abs: (value: number) => Math.abs(value),
|
|
6
|
+
floor: (value: number) => Math.floor(value),
|
|
7
|
+
ceil: (value: number) => Math.ceil(value),
|
|
8
|
+
sign: (value: number) => Math.sign(value),
|
|
9
|
+
sqrt: (value: number) => Math.sqrt(value),
|
|
10
|
+
isEven: (value: number) => value % 2 === 0,
|
|
11
|
+
isOdd: (value: number) => Math.abs(value % 2) === 1,
|
|
12
|
+
round: (value: number) => nativeFn((args) => roundTo(value, Number(args[0] ?? 0))),
|
|
13
|
+
toFixed: (value: number) => nativeFn((args) => value.toFixed(places(args[0]))),
|
|
14
|
+
clamp: (value: number) =>
|
|
15
|
+
nativeFn((args) => Math.min(Math.max(value, Number(args[0])), Number(args[1]))),
|
|
16
|
+
pow: (value: number) => nativeFn((args) => value ** Number(args[0])),
|
|
17
|
+
times: (value: number) => Array.from({ length: Math.max(0, Math.trunc(value)) }, (_x, i) => i),
|
|
18
|
+
toString: (value: number) => String(value),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function roundTo(value: number, decimals: number): number {
|
|
22
|
+
const scale = 10 ** places(decimals);
|
|
23
|
+
return Math.round(value * scale) / scale;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function places(value: unknown): number {
|
|
27
|
+
const parsed = Math.trunc(Number(value ?? 0));
|
|
28
|
+
return Number.isNaN(parsed) ? 0 : Math.min(20, Math.max(0, parsed));
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Method, nativeFn } from "../native.types.js";
|
|
2
|
+
|
|
3
|
+
const COMBINING = /[\u0300-\u036f]/g;
|
|
4
|
+
|
|
5
|
+
/** Text handling a script actually needs: words, lines, casing, slugs, matching. */
|
|
6
|
+
export const STRING_EXTRAS: Record<string, Method> = {
|
|
7
|
+
words: (text: string) => text.split(/\s+/).filter(Boolean),
|
|
8
|
+
lines: (text: string) => text.split(/\r?\n/),
|
|
9
|
+
chars: (text: string) => [...text],
|
|
10
|
+
capitalize: (text: string) => text.charAt(0).toUpperCase() + text.slice(1),
|
|
11
|
+
title: (text: string) => text.replace(/\S+/g, capitalizeWord),
|
|
12
|
+
slugify: (text: string) => slugify(text),
|
|
13
|
+
isEmpty: (text: string) => text.length === 0,
|
|
14
|
+
isBlank: (text: string) => text.trim().length === 0,
|
|
15
|
+
trimStart: (text: string) => text.trimStart(),
|
|
16
|
+
trimEnd: (text: string) => text.trimEnd(),
|
|
17
|
+
count: (text: string) => nativeFn((args) => occurrences(text, String(args[0] ?? ""))),
|
|
18
|
+
matches: (text: string) => nativeFn((args) => matches(text, String(args[0] ?? ""))),
|
|
19
|
+
test: (text: string) => nativeFn((args) => regex(String(args[0] ?? ""))?.test(text) ?? false),
|
|
20
|
+
before: (text: string) => nativeFn((args) => cut(text, String(args[0] ?? ""), true)),
|
|
21
|
+
after: (text: string) => nativeFn((args) => cut(text, String(args[0] ?? ""), false)),
|
|
22
|
+
ensureStart: (text: string) => nativeFn((args) => prefixed(text, String(args[0] ?? ""))),
|
|
23
|
+
ensureEnd: (text: string) => nativeFn((args) => suffixed(text, String(args[0] ?? ""))),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function capitalizeWord(word: string): string {
|
|
27
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `"João Gonçalves"` becomes `"joao-goncalves"`: ASCII-safe, url-ready. */
|
|
31
|
+
function slugify(text: string): string {
|
|
32
|
+
const plain = text.normalize("NFD").replace(COMBINING, "").toLowerCase();
|
|
33
|
+
return plain.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function occurrences(text: string, needle: string): number {
|
|
37
|
+
return needle === "" ? 0 : text.split(needle).length - 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every match of a pattern, as a list of strings. An invalid pattern matches nothing. */
|
|
41
|
+
function matches(text: string, pattern: string): string[] {
|
|
42
|
+
const expression = regex(pattern, "g");
|
|
43
|
+
return expression ? [...text.matchAll(expression)].map((match) => match[0]) : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function regex(pattern: string, flags = ""): RegExp | undefined {
|
|
47
|
+
try {
|
|
48
|
+
return new RegExp(pattern, flags);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cut(text: string, marker: string, keepHead: boolean): string {
|
|
55
|
+
const at = text.indexOf(marker);
|
|
56
|
+
if (at < 0) return keepHead ? text : "";
|
|
57
|
+
return keepHead ? text.slice(0, at) : text.slice(at + marker.length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function prefixed(text: string, prefix: string): string {
|
|
61
|
+
return text.startsWith(prefix) ? text : `${prefix}${text}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function suffixed(text: string, suffix: string): string {
|
|
65
|
+
return text.endsWith(suffix) ? text : `${text}${suffix}`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Method, nativeFn } from "../native.types.js";
|
|
2
|
+
|
|
3
|
+
function end(value: unknown): number | undefined {
|
|
4
|
+
return value === undefined ? undefined : Number(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Members of a string value. A no-argument transform is a property, read
|
|
9
|
+
* without parentheses (`name.upper`); anything taking an argument is a method
|
|
10
|
+
* (`name.split(",")`). `replace` replaces every occurrence.
|
|
11
|
+
*/
|
|
12
|
+
export const STRING_METHODS: Record<string, Method> = {
|
|
13
|
+
len: (text: string) => text.length,
|
|
14
|
+
upper: (text: string) => text.toUpperCase(),
|
|
15
|
+
lower: (text: string) => text.toLowerCase(),
|
|
16
|
+
trim: (text: string) => text.trim(),
|
|
17
|
+
reverse: (text: string) => [...text].reverse().join(""),
|
|
18
|
+
toNumber: (text: string) => Number(text),
|
|
19
|
+
split: (text: string) => nativeFn((args) => text.split(String(args[0] ?? ""))),
|
|
20
|
+
replace: (text: string) =>
|
|
21
|
+
nativeFn((args) => text.split(String(args[0])).join(String(args[1] ?? ""))),
|
|
22
|
+
contains: (text: string) => nativeFn((args) => text.includes(String(args[0]))),
|
|
23
|
+
startsWith: (text: string) => nativeFn((args) => text.startsWith(String(args[0]))),
|
|
24
|
+
endsWith: (text: string) => nativeFn((args) => text.endsWith(String(args[0]))),
|
|
25
|
+
slice: (text: string) => nativeFn((args) => text.slice(Number(args[0] ?? 0), end(args[1]))),
|
|
26
|
+
repeat: (text: string) => nativeFn((args) => text.repeat(Math.max(0, Number(args[0] ?? 0)))),
|
|
27
|
+
padStart: (text: string) =>
|
|
28
|
+
nativeFn((args) => text.padStart(Number(args[0] ?? 0), String(args[1] ?? " "))),
|
|
29
|
+
padEnd: (text: string) =>
|
|
30
|
+
nativeFn((args) => text.padEnd(Number(args[0] ?? 0), String(args[1] ?? " "))),
|
|
31
|
+
indexOf: (text: string) => nativeFn((args) => text.indexOf(String(args[0]))),
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Duration, Percent, Size } from "../../units/index.js";
|
|
2
|
+
import { type Method, nativeFn } from "../native.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reading a unit back as a plain number, in whichever unit you want it.
|
|
6
|
+
*
|
|
7
|
+
* A `duration` knows it is a duration; what it does not know is whether you
|
|
8
|
+
* want to print milliseconds or minutes. These are the way across, and they all
|
|
9
|
+
* land on `number`, the unit having served its purpose by then.
|
|
10
|
+
*/
|
|
11
|
+
export const DURATION_METHODS: Record<string, Method> = {
|
|
12
|
+
ms: (value: Duration) => value.ms,
|
|
13
|
+
seconds: (value: Duration) => value.ms / 1000,
|
|
14
|
+
minutes: (value: Duration) => value.ms / 60000,
|
|
15
|
+
hours: (value: Duration) => value.ms / 3600000,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SIZE_METHODS: Record<string, Method> = {
|
|
19
|
+
bytes: (value: Size) => value.bytes,
|
|
20
|
+
kb: (value: Size) => value.bytes / 1024,
|
|
21
|
+
mb: (value: Size) => value.bytes / 1048576,
|
|
22
|
+
gb: (value: Size) => value.bytes / 1073741824,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PERCENT_METHODS: Record<string, Method> = {
|
|
26
|
+
ratio: (value: Percent) => value.ratio,
|
|
27
|
+
percent: (value: Percent) => value.ratio * 100,
|
|
28
|
+
// `12%.of(50)` is 6: the share of something, which is what a percent is for.
|
|
29
|
+
of: (value: Percent) => nativeFn((args) => Number(args[0]) * value.ratio),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reading a plain number *as* a unit: the way back.
|
|
34
|
+
*
|
|
35
|
+
* Symmetric with the tables above. `2mb.kb` writes a size out as 2048 and
|
|
36
|
+
* `2048.toKb` reads 2048 back as that size, so for every `X` a unit answers to,
|
|
37
|
+
* a number answers to `toX`.
|
|
38
|
+
*/
|
|
39
|
+
export const NUMBER_TO_UNIT: Record<string, Method> = {
|
|
40
|
+
toMs: (value: number) => duration(value),
|
|
41
|
+
toSeconds: (value: number) => duration(value * 1000),
|
|
42
|
+
toMinutes: (value: number) => duration(value * 60000),
|
|
43
|
+
toHours: (value: number) => duration(value * 3600000),
|
|
44
|
+
toBytes: (value: number) => size(value),
|
|
45
|
+
toKb: (value: number) => size(value * 1024),
|
|
46
|
+
toMb: (value: number) => size(value * 1048576),
|
|
47
|
+
toGb: (value: number) => size(value * 1073741824),
|
|
48
|
+
toRatio: (value: number) => percent(value),
|
|
49
|
+
toPercent: (value: number) => percent(value / 100),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const duration = (ms: number): Duration => ({ kind: "duration", ms });
|
|
53
|
+
const size = (bytes: number): Size => ({ kind: "size", bytes });
|
|
54
|
+
const percent = (ratio: number): Percent => ({ kind: "percent", ratio });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const NAMESPACE = Symbol("venn.namespace");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A plugin namespace as a value (`fmt`, `data`, `crypto`), so its verbs can be
|
|
5
|
+
* called from any expression: `print(fmt.table(rows))`.
|
|
6
|
+
*
|
|
7
|
+
* Marked, because a name the user binds must still win over a namespace of the
|
|
8
|
+
* same name, and a bare `data.faker.uuid` must still read as a call rather than
|
|
9
|
+
* as the function itself.
|
|
10
|
+
*/
|
|
11
|
+
export function namespaceValue(members: Record<string, unknown>): Record<string, unknown> {
|
|
12
|
+
return Object.assign(Object.create(null) as Record<string, unknown>, members, {
|
|
13
|
+
[NAMESPACE]: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Whether this value is a namespace rather than an ordinary map. */
|
|
18
|
+
export function isNamespaceValue(value: unknown): boolean {
|
|
19
|
+
return typeof value === "object" && value !== null && NAMESPACE in value;
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoke a Venn callable (a closure or another native fn) with values.
|
|
3
|
+
*
|
|
4
|
+
* The fixed arities are part of the contract because the built-in methods are
|
|
5
|
+
* the hottest callers there are: `map` and `reduce` call once per element, and
|
|
6
|
+
* an argument list would be one array per element. Injected rather than
|
|
7
|
+
* imported, so a method never depends on the invoker itself.
|
|
8
|
+
*/
|
|
9
|
+
export interface Invoke {
|
|
10
|
+
(fn: unknown, values: readonly unknown[]): unknown;
|
|
11
|
+
one(fn: unknown, a: unknown): unknown;
|
|
12
|
+
two(fn: unknown, a: unknown, b: unknown): unknown;
|
|
13
|
+
three(fn: unknown, a: unknown, b: unknown, c: unknown): unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A built-in method on a native value: `[1,2].map`, `"a".upper`. */
|
|
17
|
+
export type Method = (receiver: never, invoke: Invoke) => unknown;
|
|
18
|
+
|
|
19
|
+
const NATIVE = Symbol("venn.native");
|
|
20
|
+
|
|
21
|
+
/** A callable backed by a host function rather than by Venn source. */
|
|
22
|
+
export interface NativeFn {
|
|
23
|
+
readonly [NATIVE]: true;
|
|
24
|
+
call(values: readonly unknown[]): unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether this value is a host-backed callable. */
|
|
28
|
+
export function isNativeFn(value: unknown): value is NativeFn {
|
|
29
|
+
return typeof value === "object" && value !== null && NATIVE in value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a host function so the language can call it.
|
|
34
|
+
*
|
|
35
|
+
* The wrapper takes the whole argument list: arity is the host function's own
|
|
36
|
+
* business, and a plugin verb reads what it needs and ignores the rest.
|
|
37
|
+
*/
|
|
38
|
+
export function nativeFn(call: (values: readonly unknown[]) => unknown): NativeFn {
|
|
39
|
+
return { [NATIVE]: true, call };
|
|
40
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { buildProblem, CODES } from "../codes/index.js";
|
|
2
|
+
import { ProblemError } from "../problem/index.js";
|
|
3
|
+
import { combine, type Numeric } from "../units/index.js";
|
|
4
|
+
import { isNumeric, strictEquals } from "../value/index.js";
|
|
5
|
+
import { isWaiting, whenBothReady } from "./pending.js";
|
|
6
|
+
|
|
7
|
+
const NO_SPAN = { uri: "", offset: 0, length: 0, line: 1, column: 1 };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Two plain numbers, where the unit machinery has nothing to decide. Going
|
|
11
|
+
* through `combine` for `x + 1` normalises both sides into fresh objects and
|
|
12
|
+
* asks whether their units agree, which for scalars is always yes.
|
|
13
|
+
*
|
|
14
|
+
* The compiler looks the operator up here once, so at run time this is a
|
|
15
|
+
* captured function rather than a table read.
|
|
16
|
+
*/
|
|
17
|
+
export const PLAIN: Record<string, (a: number, b: number) => number | boolean> = {
|
|
18
|
+
"+": (a, b) => a + b,
|
|
19
|
+
"-": (a, b) => a - b,
|
|
20
|
+
"*": (a, b) => a * b,
|
|
21
|
+
"/": (a, b) => a / b,
|
|
22
|
+
"%": (a, b) => a % b,
|
|
23
|
+
"<": (a, b) => a < b,
|
|
24
|
+
"<=": (a, b) => a <= b,
|
|
25
|
+
">": (a, b) => a > b,
|
|
26
|
+
">=": (a, b) => a >= b,
|
|
27
|
+
"==": (a, b) => a === b,
|
|
28
|
+
"!=": (a, b) => a !== b,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply a binary operator to two values of any kind.
|
|
33
|
+
*
|
|
34
|
+
* @throws ProblemError VN3012 when the units do not agree, or when the operator
|
|
35
|
+
* has no meaning for these values.
|
|
36
|
+
*/
|
|
37
|
+
export function applyBinary(op: string, left: unknown, right: unknown): unknown {
|
|
38
|
+
// Only on the slow path: two plain numbers never reach here. Either side may
|
|
39
|
+
// still be on its way, in which case the answer is too.
|
|
40
|
+
if (isWaiting(left) || isWaiting(right)) {
|
|
41
|
+
return whenBothReady(left, right, (a, b) => applyBinary(op, a, b));
|
|
42
|
+
}
|
|
43
|
+
if (op === "in") return isIn(left, right);
|
|
44
|
+
if (op === "~=") return regexMatch(left, right);
|
|
45
|
+
if (isNumeric(left) && isNumeric(right)) return numeric(op, left, right);
|
|
46
|
+
if (op === "==") return strictEquals(left, right);
|
|
47
|
+
if (op === "!=") return !strictEquals(left, right);
|
|
48
|
+
throw operatorError(op);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Negate a numeric, keeping its unit. */
|
|
52
|
+
export function negate(value: Numeric): unknown {
|
|
53
|
+
const result = combine({ op: "*", left: value, right: -1 });
|
|
54
|
+
return result.ok ? result.value : value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function numeric(op: string, left: Numeric, right: Numeric): unknown {
|
|
58
|
+
const result = combine({ op, left, right });
|
|
59
|
+
if (result.ok) return result.value;
|
|
60
|
+
throw new ProblemError(
|
|
61
|
+
buildProblem({
|
|
62
|
+
spec: CODES.VN3012_UNIT_MISMATCH,
|
|
63
|
+
span: NO_SPAN,
|
|
64
|
+
title: `Cannot combine ${result.mismatch.left} with ${result.mismatch.right} using "${op}".`,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isIn(left: unknown, right: unknown): boolean {
|
|
70
|
+
if (Array.isArray(right)) return right.includes(left);
|
|
71
|
+
if (typeof right === "string") return right.includes(String(left));
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function regexMatch(subject: unknown, pattern: unknown): boolean {
|
|
76
|
+
try {
|
|
77
|
+
return new RegExp(String(pattern)).test(String(subject));
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function operatorError(op: string): ProblemError {
|
|
84
|
+
return new ProblemError(
|
|
85
|
+
buildProblem({
|
|
86
|
+
spec: CODES.VN3012_UNIT_MISMATCH,
|
|
87
|
+
span: NO_SPAN,
|
|
88
|
+
title: `Operator "${op}" cannot be applied to these values.`,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Values that have not arrived yet, travelling up an expression.
|
|
3
|
+
*
|
|
4
|
+
* Expressions compile to synchronous functions, so a plugin verb called from
|
|
5
|
+
* inside one hands back the promise it is running on rather than its result.
|
|
6
|
+
* Computing with that promise would quietly produce nonsense, so each node that
|
|
7
|
+
* meets one chains onto it and hands a promise to *its* parent. The statement
|
|
8
|
+
* at the top is already asynchronous and waits there.
|
|
9
|
+
*
|
|
10
|
+
* The effect is that `await` never has to be written: a function reaching for
|
|
11
|
+
* something slow returns something slow, all the way up, and the statement that
|
|
12
|
+
* binds it gets the value. Nothing pays for this until a promise appears, and
|
|
13
|
+
* the checks sit past the fast paths ordinary values take.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Whether this value is still on its way. */
|
|
17
|
+
export function isWaiting(value: unknown): value is Promise<unknown> {
|
|
18
|
+
return value instanceof Promise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Continue once this value has arrived, or immediately if it already has. */
|
|
22
|
+
export function whenReady<T>(value: unknown, then: (settled: unknown) => T): T | Promise<T> {
|
|
23
|
+
return isWaiting(value) ? value.then(then) : then(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The same for a pair, without building an array when neither is waiting. */
|
|
27
|
+
export function whenBothReady<T>(
|
|
28
|
+
left: unknown,
|
|
29
|
+
right: unknown,
|
|
30
|
+
then: (a: unknown, b: unknown) => T,
|
|
31
|
+
): T | Promise<T> {
|
|
32
|
+
if (!isWaiting(left) && !isWaiting(right)) return then(left, right);
|
|
33
|
+
return Promise.all([left, right]).then(([a, b]) => then(a, b));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The same for a list: `Promise.all` only when one of them is waiting. */
|
|
37
|
+
export function whenAllReady<T>(
|
|
38
|
+
values: readonly unknown[],
|
|
39
|
+
then: (settled: unknown[]) => T,
|
|
40
|
+
): T | Promise<T> {
|
|
41
|
+
// An index loop, not `for…of`: this runs once per call and the iterator a
|
|
42
|
+
// `for…of` allocates showed up as 25% on a call-heavy program.
|
|
43
|
+
for (let at = 0; at < values.length; at += 1) {
|
|
44
|
+
if (isWaiting(values[at])) return Promise.all(values).then(then);
|
|
45
|
+
}
|
|
46
|
+
return then(values as unknown[]);
|
|
47
|
+
}
|