trilean 0.0.0 → 1.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/LICENSE +21 -0
- package/README.md +634 -0
- package/dist/computed-value.cjs +71 -0
- package/dist/computed-value.d.cts +43 -0
- package/dist/computed-value.d.ts +43 -0
- package/dist/computed-value.js +65 -0
- package/dist/derived-aggregates.cjs +74 -0
- package/dist/derived-aggregates.d.cts +12 -0
- package/dist/derived-aggregates.d.ts +12 -0
- package/dist/derived-aggregates.js +70 -0
- package/dist/derived-connectives.cjs +40 -0
- package/dist/derived-connectives.d.cts +17 -0
- package/dist/derived-connectives.d.ts +17 -0
- package/dist/derived-connectives.js +31 -0
- package/dist/evaluation.cjs +39 -0
- package/dist/evaluation.d.cts +29 -0
- package/dist/evaluation.d.ts +29 -0
- package/dist/evaluation.js +35 -0
- package/dist/evaluator.cjs +482 -0
- package/dist/evaluator.d.cts +18 -0
- package/dist/evaluator.d.ts +18 -0
- package/dist/evaluator.js +479 -0
- package/dist/functions.cjs +5 -0
- package/dist/functions.d.cts +11 -0
- package/dist/functions.d.ts +11 -0
- package/dist/functions.js +4 -0
- package/dist/index.cjs +69 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/json-value-B1Hjjjri.d.cts +11 -0
- package/dist/json-value-B1Hjjjri.d.ts +11 -0
- package/dist/json-value.cjs +13 -0
- package/dist/json-value.d.cts +2 -0
- package/dist/json-value.d.ts +2 -0
- package/dist/json-value.js +12 -0
- package/dist/resolvers.cjs +0 -0
- package/dist/resolvers.d.cts +28 -0
- package/dist/resolvers.d.ts +28 -0
- package/dist/resolvers.js +0 -0
- package/dist/tree.cjs +289 -0
- package/dist/tree.d.cts +368 -0
- package/dist/tree.d.ts +368 -0
- package/dist/tree.js +257 -0
- package/package.json +114 -2
- package/schemas/trilean.schema.json +1 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_computed_value = require("./computed-value.cjs");
|
|
3
|
+
const require_evaluation = require("./evaluation.cjs");
|
|
4
|
+
const require_functions = require("./functions.cjs");
|
|
5
|
+
//#region src/evaluator.ts
|
|
6
|
+
/**
|
|
7
|
+
* `evaluatePredicateInternal` and `evaluateValueInternal` are co-located in this one file, rather than split across `predicate-evaluator.ts`/`value-evaluator.ts`, because they are mutually recursive: a predicate leaf (`compare`, `textCompare`, `memberOf`, `exists`) holds `ExpressionNode` operands, and an expression node (`conditional`'s `when`, a fold's `filter`) holds `PredicateNode` operands. Splitting them across modules would make each half import the other, and whichever module finished loading second would see the other's export as `undefined` at its own module-evaluation time -- a genuine circular-import TDZ hazard, not merely a style preference.
|
|
8
|
+
*/
|
|
9
|
+
/** The three-valued AND table: `false` is absorbing regardless of the other operand's indeterminacy; otherwise both-definite folds to a boolean AND; otherwise indeterminate, with the tie-break rule (declared operand order, left before right) applied via `firstIndeterminate`. */
|
|
10
|
+
function combineAnd(left, right) {
|
|
11
|
+
if (left.status === "definite" && !left.value) return require_evaluation.definite(false);
|
|
12
|
+
if (right.status === "definite" && !right.value) return require_evaluation.definite(false);
|
|
13
|
+
const reason = require_evaluation.firstIndeterminate(left, right);
|
|
14
|
+
if (reason !== void 0) return {
|
|
15
|
+
status: "indeterminate",
|
|
16
|
+
reason
|
|
17
|
+
};
|
|
18
|
+
return require_evaluation.definite(true);
|
|
19
|
+
}
|
|
20
|
+
/** The three-valued OR table: mirror image of `combineAnd`, with `true` absorbing. */
|
|
21
|
+
function combineOr(left, right) {
|
|
22
|
+
if (left.status === "definite" && left.value) return require_evaluation.definite(true);
|
|
23
|
+
if (right.status === "definite" && right.value) return require_evaluation.definite(true);
|
|
24
|
+
const reason = require_evaluation.firstIndeterminate(left, right);
|
|
25
|
+
if (reason !== void 0) return {
|
|
26
|
+
status: "indeterminate",
|
|
27
|
+
reason
|
|
28
|
+
};
|
|
29
|
+
return require_evaluation.definite(false);
|
|
30
|
+
}
|
|
31
|
+
function applyComparisonOperator(op, left, right) {
|
|
32
|
+
switch (op) {
|
|
33
|
+
case "gt": return left > right;
|
|
34
|
+
case "gte": return left >= right;
|
|
35
|
+
case "lt": return left < right;
|
|
36
|
+
case "lte": return left <= right;
|
|
37
|
+
case "eq": return left === right;
|
|
38
|
+
case "neq": return left !== right;
|
|
39
|
+
default: throw new Error("unreachable comparison operator");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const millisecondsPerDurationUnit = {
|
|
43
|
+
ms: 1,
|
|
44
|
+
s: 1e3,
|
|
45
|
+
min: 6e4,
|
|
46
|
+
h: 36e5,
|
|
47
|
+
d: 864e5
|
|
48
|
+
};
|
|
49
|
+
/** Normalises a `duration`'s magnitude to milliseconds, the common base unit for combining or comparing two `duration`s (or an `instant` and a `duration`) of potentially different `DurationUnit`s -- shared by `compareValues`'s own `duration` branch below and by the temporal arithmetic further down. */
|
|
50
|
+
function toMilliseconds(value, unit) {
|
|
51
|
+
return value * millisecondsPerDurationUnit[unit];
|
|
52
|
+
}
|
|
53
|
+
/** `Date.parse` reports an unparseable timestamp as `NaN`, and every downstream use of that `NaN` then fails silently rather than loudly: every comparison against it is `false` (so `neq` between two unparseable instants would come back definitely `true`), a subtraction yields a `NaN`-magnitude duration, and `new Date(NaN).toISOString()` throws a `RangeError` outright -- a thrown exception for a data-quality problem, which this design never does (see `Evaluation<T>`'s own doc comment in evaluation.ts). An `instant` whose string will not parse is a value the operation cannot use, so every caller below reports it as `wrong-type`, exactly like any other unusable operand. */
|
|
54
|
+
function toEpochMilliseconds(value) {
|
|
55
|
+
const epochMilliseconds = Date.parse(value);
|
|
56
|
+
return Number.isNaN(epochMilliseconds) ? void 0 : epochMilliseconds;
|
|
57
|
+
}
|
|
58
|
+
function unparseableInstant(value) {
|
|
59
|
+
return require_evaluation.indeterminate("wrong-type", `'${value}' is not a parseable ISO-8601 timestamp`);
|
|
60
|
+
}
|
|
61
|
+
/** The representable timestamp range is finite, so a valid instant shifted by a large enough duration lands outside it, where `new Date(...).toISOString()` throws a `RangeError`. Checking the shifted date against the platform's own notion of a valid time value keeps that inside the three-outcome model as a `domain-error` -- an operation pushed outside its valid domain, the same category as division by zero -- and avoids hardcoding the range bound. */
|
|
62
|
+
function instantFromEpochMilliseconds(epochMilliseconds) {
|
|
63
|
+
const shifted = new Date(epochMilliseconds);
|
|
64
|
+
if (Number.isNaN(shifted.getTime())) return require_evaluation.indeterminate("domain-error", "the resulting instant falls outside the representable timestamp range");
|
|
65
|
+
return require_evaluation.definite({
|
|
66
|
+
kind: "instant",
|
|
67
|
+
value: shifted.toISOString()
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** `compare`'s two operands: valid kinds are `number` (matching units required), `instant` (ordered by parsed epoch millisecond), or `duration` (ordered by magnitude normalised to milliseconds) -- `text` is never valid here (see `textCompare`), and a kind mismatch between the two operands is `wrong-type`. Narrowing `right` against a literal `left.kind` case (rather than asserting it) is what lets both sides stay properly typed with no `as`. */
|
|
71
|
+
function compareValues(op, left, right) {
|
|
72
|
+
switch (left.kind) {
|
|
73
|
+
case "number":
|
|
74
|
+
if (right.kind !== "number") return require_evaluation.indeterminate("wrong-type", `cannot compare a 'number' value with a '${right.kind}' value`);
|
|
75
|
+
if (!require_computed_value.unitsEqual(left.unit, right.unit)) return require_evaluation.indeterminate("wrong-type", "cannot compare numbers with incompatible units");
|
|
76
|
+
return require_evaluation.definite(applyComparisonOperator(op, left.value, right.value));
|
|
77
|
+
case "text": return require_evaluation.indeterminate("wrong-type", "text values are not comparable via 'compare'; use 'textCompare'");
|
|
78
|
+
case "instant": {
|
|
79
|
+
if (right.kind !== "instant") return require_evaluation.indeterminate("wrong-type", `cannot compare an 'instant' value with a '${right.kind}' value`);
|
|
80
|
+
const leftEpoch = toEpochMilliseconds(left.value);
|
|
81
|
+
if (leftEpoch === void 0) return unparseableInstant(left.value);
|
|
82
|
+
const rightEpoch = toEpochMilliseconds(right.value);
|
|
83
|
+
if (rightEpoch === void 0) return unparseableInstant(right.value);
|
|
84
|
+
return require_evaluation.definite(applyComparisonOperator(op, leftEpoch, rightEpoch));
|
|
85
|
+
}
|
|
86
|
+
case "duration":
|
|
87
|
+
if (right.kind !== "duration") return require_evaluation.indeterminate("wrong-type", `cannot compare a 'duration' value with a '${right.kind}' value`);
|
|
88
|
+
return require_evaluation.definite(applyComparisonOperator(op, toMilliseconds(left.value, left.unit), toMilliseconds(right.value, right.unit)));
|
|
89
|
+
default: throw new Error("unreachable computed-value kind");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** `textCompare`'s two operands must both resolve to the `text` computed-value kind -- any other kind, on either operand, is `wrong-type` (see the `textCompare` section of README.md). `equals`/`notEquals` are exact string equality; `matches`/`notMatches` interpret `right`'s text as an ECMAScript regular expression tested against `left`'s text. An invalid pattern is `wrong-type` rather than a thrown exception -- every data-quality problem stays inside the `Evaluation` result, per `Evaluation<T>`'s own doc comment in evaluation.ts. */
|
|
93
|
+
function compareText(op, left, right) {
|
|
94
|
+
if (left.kind !== "text") return require_evaluation.indeterminate("wrong-type", `'textCompare' requires 'text' operands; the left operand is '${left.kind}'`);
|
|
95
|
+
if (right.kind !== "text") return require_evaluation.indeterminate("wrong-type", `'textCompare' requires 'text' operands; the right operand is '${right.kind}'`);
|
|
96
|
+
switch (op) {
|
|
97
|
+
case "equals": return require_evaluation.definite(left.value === right.value);
|
|
98
|
+
case "notEquals": return require_evaluation.definite(left.value !== right.value);
|
|
99
|
+
case "matches":
|
|
100
|
+
case "notMatches": {
|
|
101
|
+
let pattern;
|
|
102
|
+
try {
|
|
103
|
+
pattern = new RegExp(right.value);
|
|
104
|
+
} catch {
|
|
105
|
+
return require_evaluation.indeterminate("wrong-type", `'${right.value}' is not a valid regular expression pattern`);
|
|
106
|
+
}
|
|
107
|
+
const isMatch = pattern.test(left.value);
|
|
108
|
+
return require_evaluation.definite(op === "matches" ? isMatch : !isMatch);
|
|
109
|
+
}
|
|
110
|
+
default: throw new Error("unreachable text comparison operator");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** `memberOf`'s own value-equality test between two computed values -- kind-agnostic across `number`/`text`/`instant`/`duration` (see the `memberOf` section of README.md, and its "Derived aggregates" note that this equality is kind-agnostic across every computed-value kind), unlike `compare`'s own `eq`, which rejects a `text` operand outright and directs callers to `textCompare` instead. A kind mismatch, or a `number` pair with an incompatible unit, is `wrong-type` -- never simply "not equal". Narrowing `candidate` against a literal `operand.kind` case (rather than asserting it) is the same technique `compareValues` uses above. */
|
|
114
|
+
function computeMembershipMatch(operand, candidate) {
|
|
115
|
+
switch (operand.kind) {
|
|
116
|
+
case "number":
|
|
117
|
+
if (candidate.kind !== "number") return require_evaluation.indeterminate("wrong-type", `cannot compare a 'number' value with a '${candidate.kind}' value for membership`);
|
|
118
|
+
if (!require_computed_value.unitsEqual(operand.unit, candidate.unit)) return require_evaluation.indeterminate("wrong-type", "cannot compare numbers with incompatible units for membership");
|
|
119
|
+
return require_evaluation.definite(operand.value === candidate.value);
|
|
120
|
+
case "text":
|
|
121
|
+
if (candidate.kind !== "text") return require_evaluation.indeterminate("wrong-type", `cannot compare a 'text' value with a '${candidate.kind}' value for membership`);
|
|
122
|
+
return require_evaluation.definite(operand.value === candidate.value);
|
|
123
|
+
case "instant": {
|
|
124
|
+
if (candidate.kind !== "instant") return require_evaluation.indeterminate("wrong-type", `cannot compare an 'instant' value with a '${candidate.kind}' value for membership`);
|
|
125
|
+
const operandEpoch = toEpochMilliseconds(operand.value);
|
|
126
|
+
if (operandEpoch === void 0) return unparseableInstant(operand.value);
|
|
127
|
+
const candidateEpoch = toEpochMilliseconds(candidate.value);
|
|
128
|
+
if (candidateEpoch === void 0) return unparseableInstant(candidate.value);
|
|
129
|
+
return require_evaluation.definite(operandEpoch === candidateEpoch);
|
|
130
|
+
}
|
|
131
|
+
case "duration":
|
|
132
|
+
if (candidate.kind !== "duration") return require_evaluation.indeterminate("wrong-type", `cannot compare a 'duration' value with a '${candidate.kind}' value for membership`);
|
|
133
|
+
return require_evaluation.definite(toMilliseconds(operand.value, operand.unit) === toMilliseconds(candidate.value, candidate.unit));
|
|
134
|
+
default: throw new Error("unreachable computed-value kind");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** `power`/`modulo` have no defined unit-combination rule in this design (unlike `add`/`subtract`'s "identical units" requirement or `multiply`/`divide`'s dimensional-exponent combination) -- scoping them to dimensionless operands avoids inventing an unspecified unit-scaling semantics for a fractional or runtime-determined exponent. */
|
|
138
|
+
function isDimensionless(unit) {
|
|
139
|
+
return require_computed_value.unitsEqual(unit, void 0);
|
|
140
|
+
}
|
|
141
|
+
/** `negate` is never sugar for "zero minus the value" (see the `arithmetic`/`negate` section of README.md) -- a `duration`'s magnitude is negated directly, in its own original unit, with no subtraction or millisecond normalisation involved. */
|
|
142
|
+
function applyNegate(operand) {
|
|
143
|
+
switch (operand.kind) {
|
|
144
|
+
case "number": return require_evaluation.definite({
|
|
145
|
+
kind: "number",
|
|
146
|
+
value: -operand.value,
|
|
147
|
+
unit: operand.unit
|
|
148
|
+
});
|
|
149
|
+
case "duration": return require_evaluation.definite({
|
|
150
|
+
kind: "duration",
|
|
151
|
+
value: -operand.value,
|
|
152
|
+
unit: operand.unit
|
|
153
|
+
});
|
|
154
|
+
case "text":
|
|
155
|
+
case "instant": return require_evaluation.indeterminate("wrong-type", `cannot negate a '${operand.kind}' value`);
|
|
156
|
+
default: throw new Error("unreachable computed-value kind");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function applyArithmeticOnNumbers(op, left, right) {
|
|
160
|
+
switch (op) {
|
|
161
|
+
case "add":
|
|
162
|
+
if (!require_computed_value.unitsEqual(left.unit, right.unit)) return require_evaluation.indeterminate("wrong-type", "cannot add numbers with incompatible units");
|
|
163
|
+
return require_evaluation.definite({
|
|
164
|
+
kind: "number",
|
|
165
|
+
value: left.value + right.value,
|
|
166
|
+
unit: left.unit
|
|
167
|
+
});
|
|
168
|
+
case "subtract":
|
|
169
|
+
if (!require_computed_value.unitsEqual(left.unit, right.unit)) return require_evaluation.indeterminate("wrong-type", "cannot subtract numbers with incompatible units");
|
|
170
|
+
return require_evaluation.definite({
|
|
171
|
+
kind: "number",
|
|
172
|
+
value: left.value - right.value,
|
|
173
|
+
unit: left.unit
|
|
174
|
+
});
|
|
175
|
+
case "multiply": return require_evaluation.definite({
|
|
176
|
+
kind: "number",
|
|
177
|
+
value: left.value * right.value,
|
|
178
|
+
unit: require_computed_value.combineUnitsForMultiply(left.unit, right.unit)
|
|
179
|
+
});
|
|
180
|
+
case "divide":
|
|
181
|
+
if (right.value === 0) return require_evaluation.indeterminate("domain-error", "division by zero");
|
|
182
|
+
return require_evaluation.definite({
|
|
183
|
+
kind: "number",
|
|
184
|
+
value: left.value / right.value,
|
|
185
|
+
unit: require_computed_value.combineUnitsForDivide(left.unit, right.unit)
|
|
186
|
+
});
|
|
187
|
+
case "power":
|
|
188
|
+
if (!isDimensionless(left.unit) || !isDimensionless(right.unit)) return require_evaluation.indeterminate("wrong-type", "'power' requires dimensionless operands");
|
|
189
|
+
if (left.value < 0 && !Number.isInteger(right.value)) return require_evaluation.indeterminate("domain-error", "a negative base raised to a non-integer power is not a real number");
|
|
190
|
+
if (left.value === 0 && right.value < 0) return require_evaluation.indeterminate("domain-error", "zero raised to a negative power is a division by zero");
|
|
191
|
+
return require_evaluation.definite({
|
|
192
|
+
kind: "number",
|
|
193
|
+
value: left.value ** right.value
|
|
194
|
+
});
|
|
195
|
+
case "modulo":
|
|
196
|
+
if (!isDimensionless(left.unit) || !isDimensionless(right.unit)) return require_evaluation.indeterminate("wrong-type", "'modulo' requires dimensionless operands");
|
|
197
|
+
if (right.value === 0) return require_evaluation.indeterminate("domain-error", "modulo by zero");
|
|
198
|
+
return require_evaluation.definite({
|
|
199
|
+
kind: "number",
|
|
200
|
+
value: left.value % right.value
|
|
201
|
+
});
|
|
202
|
+
default: throw new Error("unreachable arithmetic operator");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** The only same-kind, non-`number` arithmetic this design defines: two `duration`s combine by normalising both to milliseconds first (see `toMilliseconds`), reporting the result in milliseconds -- `multiply`/`divide`/`power`/`modulo` have no representable result unit for a `duration` squared or a dimensionless ratio, so they are `wrong-type` rather than invented. */
|
|
206
|
+
function applyArithmeticOnDurations(op, left, right) {
|
|
207
|
+
switch (op) {
|
|
208
|
+
case "add": return require_evaluation.definite({
|
|
209
|
+
kind: "duration",
|
|
210
|
+
value: toMilliseconds(left.value, left.unit) + toMilliseconds(right.value, right.unit),
|
|
211
|
+
unit: "ms"
|
|
212
|
+
});
|
|
213
|
+
case "subtract": return require_evaluation.definite({
|
|
214
|
+
kind: "duration",
|
|
215
|
+
value: toMilliseconds(left.value, left.unit) - toMilliseconds(right.value, right.unit),
|
|
216
|
+
unit: "ms"
|
|
217
|
+
});
|
|
218
|
+
case "multiply":
|
|
219
|
+
case "divide":
|
|
220
|
+
case "power":
|
|
221
|
+
case "modulo": return require_evaluation.indeterminate("wrong-type", `arithmetic operator '${op}' is not defined between two 'duration' values`);
|
|
222
|
+
default: throw new Error("unreachable arithmetic operator");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Dispatches `arithmetic` by operand kind. The three cross-kind temporal combinations this design defines (`instant - instant`, `instant + duration`, `duration + instant`) are checked explicitly first, in that order, against the exact operator each requires; any other combination touching an `instant` is `wrong-type` (see "Temporal values" in README.md -- e.g. adding two instants, or subtracting a `duration` from an `instant`, are deliberately *not* defined). Same-kind `duration`/`duration` combinations are delegated to `applyArithmeticOnDurations`; a `duration` paired with anything other than an `instant` or another `duration` is `wrong-type`. Everything remaining requires two `number` operands.
|
|
227
|
+
*/
|
|
228
|
+
function applyArithmetic(op, left, right) {
|
|
229
|
+
if (left.kind === "instant" && right.kind === "instant" && op === "subtract") {
|
|
230
|
+
const leftEpoch = toEpochMilliseconds(left.value);
|
|
231
|
+
if (leftEpoch === void 0) return unparseableInstant(left.value);
|
|
232
|
+
const rightEpoch = toEpochMilliseconds(right.value);
|
|
233
|
+
if (rightEpoch === void 0) return unparseableInstant(right.value);
|
|
234
|
+
return require_evaluation.definite({
|
|
235
|
+
kind: "duration",
|
|
236
|
+
value: leftEpoch - rightEpoch,
|
|
237
|
+
unit: "ms"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (left.kind === "instant" && right.kind === "duration" && op === "add") {
|
|
241
|
+
const leftEpoch = toEpochMilliseconds(left.value);
|
|
242
|
+
if (leftEpoch === void 0) return unparseableInstant(left.value);
|
|
243
|
+
return instantFromEpochMilliseconds(leftEpoch + toMilliseconds(right.value, right.unit));
|
|
244
|
+
}
|
|
245
|
+
if (left.kind === "duration" && right.kind === "instant" && op === "add") {
|
|
246
|
+
const rightEpoch = toEpochMilliseconds(right.value);
|
|
247
|
+
if (rightEpoch === void 0) return unparseableInstant(right.value);
|
|
248
|
+
return instantFromEpochMilliseconds(rightEpoch + toMilliseconds(left.value, left.unit));
|
|
249
|
+
}
|
|
250
|
+
if (left.kind === "instant" || right.kind === "instant") return require_evaluation.indeterminate("wrong-type", `arithmetic operator '${op}' is not defined between a '${left.kind}' and a '${right.kind}' value`);
|
|
251
|
+
if (left.kind === "duration" && right.kind === "duration") return applyArithmeticOnDurations(op, left, right);
|
|
252
|
+
if (left.kind === "duration" || right.kind === "duration") return require_evaluation.indeterminate("wrong-type", `arithmetic operator '${op}' is not defined between a '${left.kind}' and a '${right.kind}' value`);
|
|
253
|
+
if (left.kind !== "number") return require_evaluation.indeterminate("wrong-type", `arithmetic requires numeric operands; got a '${left.kind}' value`);
|
|
254
|
+
if (right.kind !== "number") return require_evaluation.indeterminate("wrong-type", `arithmetic requires numeric operands; got a '${right.kind}' value`);
|
|
255
|
+
return applyArithmeticOnNumbers(op, left, right);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Collection resolution shared by the quantifiers (`some`/`every`, below) and by `fold` (a later phase): resolves the opaque `collection` reference to its concrete candidate list via `resolvers.resolveCollection`, then evaluates each candidate's optional `filter` with that candidate as its own evaluation context and the accumulator reset to `undefined` -- see the README's "Evaluation context" and "Pre-filtering which items participate" sections. Deliberately stops short of deciding how an indeterminate filter combines with the rest of the surrounding node: `some`/`every` fold a filter-indeterminate item in as its own vote via the surrounding OR/AND absorption, while `fold` has no absorbing value at all and goes indeterminate outright on the same condition -- the two callers need genuinely different combination logic over these same per-item outcomes, so this helper only produces the outcomes and leaves combining them to the caller.
|
|
259
|
+
*/
|
|
260
|
+
async function resolveParticipatingItems(collection, filter, context, resolvers, functions) {
|
|
261
|
+
const candidates = await resolvers.resolveCollection(collection, context);
|
|
262
|
+
return Promise.all(candidates.map(async (item) => {
|
|
263
|
+
if (filter === void 0) return {
|
|
264
|
+
item,
|
|
265
|
+
filterOutcome: "include"
|
|
266
|
+
};
|
|
267
|
+
const filterResult = await evaluatePredicateInternal(filter, item, resolvers, void 0, functions);
|
|
268
|
+
if (filterResult.status === "indeterminate") return {
|
|
269
|
+
item,
|
|
270
|
+
filterOutcome: filterResult
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
item,
|
|
274
|
+
filterOutcome: filterResult.value ? "include" : "exclude"
|
|
275
|
+
};
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
/** The first participating item (in declared collection order) whose `filter` evaluation was itself indeterminate, or `undefined` if every participating item's filter resolved definitely -- a filter-excluded item's own `"exclude"` outcome never counts here. Used only by `fold`, which -- unlike `some`/`every`'s OR/AND absorption -- has no absorbing value at all: any participating item's indeterminate filter makes the whole fold indeterminate outright, with no other item's outcome able to override it. */
|
|
279
|
+
function firstFilterIndeterminate(participating) {
|
|
280
|
+
for (const { filterOutcome } of participating) {
|
|
281
|
+
if (filterOutcome === "include" || filterOutcome === "exclude") continue;
|
|
282
|
+
if (filterOutcome.status === "indeterminate") return filterOutcome.reason;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function evaluatePredicateInternal(node, context, resolvers, accumulator, functions) {
|
|
286
|
+
switch (node.kind) {
|
|
287
|
+
case "not": {
|
|
288
|
+
const operand = await evaluatePredicateInternal(node.operand, context, resolvers, accumulator, functions);
|
|
289
|
+
if (operand.status === "indeterminate") return operand;
|
|
290
|
+
return require_evaluation.definite(!operand.value);
|
|
291
|
+
}
|
|
292
|
+
case "and": {
|
|
293
|
+
const [left, right] = await Promise.all([evaluatePredicateInternal(node.left, context, resolvers, accumulator, functions), evaluatePredicateInternal(node.right, context, resolvers, accumulator, functions)]);
|
|
294
|
+
return combineAnd(left, right);
|
|
295
|
+
}
|
|
296
|
+
case "or": {
|
|
297
|
+
const [left, right] = await Promise.all([evaluatePredicateInternal(node.left, context, resolvers, accumulator, functions), evaluatePredicateInternal(node.right, context, resolvers, accumulator, functions)]);
|
|
298
|
+
return combineOr(left, right);
|
|
299
|
+
}
|
|
300
|
+
case "allOf": return (await Promise.all(node.operands.map(async (operand) => evaluatePredicateInternal(operand, context, resolvers, accumulator, functions)))).reduce(combineAnd, require_evaluation.definite(true));
|
|
301
|
+
case "anyOf": return (await Promise.all(node.operands.map(async (operand) => evaluatePredicateInternal(operand, context, resolvers, accumulator, functions)))).reduce(combineOr, require_evaluation.definite(false));
|
|
302
|
+
case "compare": {
|
|
303
|
+
const [left, right] = await Promise.all([evaluateValueInternal(node.left, context, resolvers, accumulator, functions), evaluateValueInternal(node.right, context, resolvers, accumulator, functions)]);
|
|
304
|
+
if (left.status === "indeterminate") return left;
|
|
305
|
+
if (right.status === "indeterminate") return right;
|
|
306
|
+
return compareValues(node.op, left.value, right.value);
|
|
307
|
+
}
|
|
308
|
+
case "textCompare": {
|
|
309
|
+
const [left, right] = await Promise.all([evaluateValueInternal(node.left, context, resolvers, accumulator, functions), evaluateValueInternal(node.right, context, resolvers, accumulator, functions)]);
|
|
310
|
+
if (left.status === "indeterminate") return left;
|
|
311
|
+
if (right.status === "indeterminate") return right;
|
|
312
|
+
return compareText(node.op, left.value, right.value);
|
|
313
|
+
}
|
|
314
|
+
case "memberOf": {
|
|
315
|
+
const operandResult = await evaluateValueInternal(node.operand, context, resolvers, accumulator, functions);
|
|
316
|
+
if (operandResult.status === "indeterminate") return operandResult;
|
|
317
|
+
const candidateOutcomes = await Promise.all(node.candidates.map(async (candidate) => {
|
|
318
|
+
const candidateResult = await evaluateValueInternal(candidate, context, resolvers, accumulator, functions);
|
|
319
|
+
if (candidateResult.status === "indeterminate") return candidateResult;
|
|
320
|
+
return computeMembershipMatch(operandResult.value, candidateResult.value);
|
|
321
|
+
}));
|
|
322
|
+
for (const outcome of candidateOutcomes) if (outcome.status === "definite" && outcome.value) return require_evaluation.definite(node.op === "in");
|
|
323
|
+
const reason = require_evaluation.firstIndeterminate(...candidateOutcomes);
|
|
324
|
+
if (reason !== void 0) return {
|
|
325
|
+
status: "indeterminate",
|
|
326
|
+
reason
|
|
327
|
+
};
|
|
328
|
+
return require_evaluation.definite(node.op === "notIn");
|
|
329
|
+
}
|
|
330
|
+
case "exists": {
|
|
331
|
+
const operandResult = await evaluateValueInternal(node.operand, context, resolvers, accumulator, functions);
|
|
332
|
+
if (operandResult.status === "indeterminate" && operandResult.reason.code === "not-found") return require_evaluation.definite(false);
|
|
333
|
+
return require_evaluation.definite(true);
|
|
334
|
+
}
|
|
335
|
+
case "some":
|
|
336
|
+
case "every": {
|
|
337
|
+
const participating = await resolveParticipatingItems(node.collection, node.filter, context, resolvers, functions);
|
|
338
|
+
const votes = (await Promise.all(participating.map(async ({ item, filterOutcome }) => {
|
|
339
|
+
if (filterOutcome === "exclude") return void 0;
|
|
340
|
+
if (filterOutcome !== "include") return filterOutcome;
|
|
341
|
+
return evaluatePredicateInternal(node.item, item, resolvers, void 0, functions);
|
|
342
|
+
}))).filter((vote) => vote !== void 0);
|
|
343
|
+
const combine = node.kind === "some" ? combineOr : combineAnd;
|
|
344
|
+
const identity = node.kind === "some" ? require_evaluation.definite(false) : require_evaluation.definite(true);
|
|
345
|
+
return votes.reduce(combine, identity);
|
|
346
|
+
}
|
|
347
|
+
default: throw new Error("unreachable predicate node kind");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function evaluateValueInternal(node, context, resolvers, accumulator, functions) {
|
|
351
|
+
switch (node.kind) {
|
|
352
|
+
case "reference": {
|
|
353
|
+
const resolution = await resolvers.resolveValue(node.key, context);
|
|
354
|
+
if (!resolution.found) return require_evaluation.indeterminate("not-found", `no value found for reference key ${JSON.stringify(node.key)}`);
|
|
355
|
+
if (node.unit !== void 0) {
|
|
356
|
+
if (resolution.value.kind !== "number") return require_evaluation.indeterminate("wrong-type", "a unit was expected on a reference that resolved to a non-numeric value");
|
|
357
|
+
if (!require_computed_value.unitsEqual(node.unit, resolution.value.unit)) return require_evaluation.indeterminate("wrong-type", "the resolved value's unit does not match the reference's expected unit");
|
|
358
|
+
}
|
|
359
|
+
return require_evaluation.definite(resolution.value);
|
|
360
|
+
}
|
|
361
|
+
case "accumulator":
|
|
362
|
+
if (accumulator === void 0) return require_evaluation.indeterminate("wrong-type", "accumulator used outside a reduce fold's combine expression");
|
|
363
|
+
return require_evaluation.definite(accumulator);
|
|
364
|
+
case "call": {
|
|
365
|
+
const argResults = await Promise.all(node.args.map(async (arg) => evaluateValueInternal(arg, context, resolvers, accumulator, functions)));
|
|
366
|
+
const args = [];
|
|
367
|
+
for (const result of argResults) {
|
|
368
|
+
if (result.status === "indeterminate") return result;
|
|
369
|
+
args.push(result.value);
|
|
370
|
+
}
|
|
371
|
+
const fn = Object.hasOwn(functions, node.fn) ? functions[node.fn] : void 0;
|
|
372
|
+
if (fn === void 0) return require_evaluation.indeterminate("wrong-type", `no function registered under the name '${node.fn}'`);
|
|
373
|
+
const outcome = fn(args);
|
|
374
|
+
if ("domainError" in outcome) return require_evaluation.indeterminate("domain-error", outcome.domainError);
|
|
375
|
+
return require_evaluation.definite(outcome);
|
|
376
|
+
}
|
|
377
|
+
case "numberLiteral": return require_evaluation.definite({
|
|
378
|
+
kind: "number",
|
|
379
|
+
value: node.value,
|
|
380
|
+
unit: node.unit
|
|
381
|
+
});
|
|
382
|
+
case "textLiteral": return require_evaluation.definite({
|
|
383
|
+
kind: "text",
|
|
384
|
+
value: node.value
|
|
385
|
+
});
|
|
386
|
+
case "instantLiteral": return require_evaluation.definite({
|
|
387
|
+
kind: "instant",
|
|
388
|
+
value: node.value
|
|
389
|
+
});
|
|
390
|
+
case "durationLiteral": return require_evaluation.definite({
|
|
391
|
+
kind: "duration",
|
|
392
|
+
value: node.value,
|
|
393
|
+
unit: node.unit
|
|
394
|
+
});
|
|
395
|
+
case "arithmetic": {
|
|
396
|
+
const [left, right] = await Promise.all([evaluateValueInternal(node.left, context, resolvers, accumulator, functions), evaluateValueInternal(node.right, context, resolvers, accumulator, functions)]);
|
|
397
|
+
if (left.status === "indeterminate") return left;
|
|
398
|
+
if (right.status === "indeterminate") return right;
|
|
399
|
+
return applyArithmetic(node.op, left.value, right.value);
|
|
400
|
+
}
|
|
401
|
+
case "negate": {
|
|
402
|
+
const operand = await evaluateValueInternal(node.operand, context, resolvers, accumulator, functions);
|
|
403
|
+
if (operand.status === "indeterminate") return operand;
|
|
404
|
+
return applyNegate(operand.value);
|
|
405
|
+
}
|
|
406
|
+
case "lookup": {
|
|
407
|
+
const keyResults = await Promise.all(node.keys.map(async (key) => evaluateValueInternal(key, context, resolvers, accumulator, functions)));
|
|
408
|
+
const keyValues = [];
|
|
409
|
+
for (const result of keyResults) {
|
|
410
|
+
if (result.status === "indeterminate") return result;
|
|
411
|
+
keyValues.push(result.value);
|
|
412
|
+
}
|
|
413
|
+
const resolution = await resolvers.resolveLookup(node.table, keyValues, context);
|
|
414
|
+
if (!resolution.found) return require_evaluation.indeterminate("not-found", `no match found in lookup table ${JSON.stringify(node.table)}`);
|
|
415
|
+
return require_evaluation.definite(resolution.value);
|
|
416
|
+
}
|
|
417
|
+
case "conditional":
|
|
418
|
+
for (const { when, then } of node.cases) {
|
|
419
|
+
const whenResult = await evaluatePredicateInternal(when, context, resolvers, accumulator, functions);
|
|
420
|
+
if (whenResult.status === "indeterminate") return whenResult;
|
|
421
|
+
if (whenResult.value) return evaluateValueInternal(then, context, resolvers, accumulator, functions);
|
|
422
|
+
}
|
|
423
|
+
return evaluateValueInternal(node.fallback, context, resolvers, accumulator, functions);
|
|
424
|
+
case "fold": {
|
|
425
|
+
const participating = await resolveParticipatingItems(node.collection, node.filter, context, resolvers, functions);
|
|
426
|
+
const filterIndeterminateReason = firstFilterIndeterminate(participating);
|
|
427
|
+
if (filterIndeterminateReason !== void 0) return {
|
|
428
|
+
status: "indeterminate",
|
|
429
|
+
reason: filterIndeterminateReason
|
|
430
|
+
};
|
|
431
|
+
const includedItems = participating.filter(({ filterOutcome }) => filterOutcome === "include").map(({ item }) => item);
|
|
432
|
+
if (node.combiner.mode === "reduce") {
|
|
433
|
+
const initialResult = await evaluateValueInternal(node.combiner.initial, context, resolvers, void 0, functions);
|
|
434
|
+
if (initialResult.status === "indeterminate") return initialResult;
|
|
435
|
+
let runningAccumulator = initialResult.value;
|
|
436
|
+
for (const item of includedItems) {
|
|
437
|
+
const stepResult = await evaluateValueInternal(node.combiner.combine, item, resolvers, runningAccumulator, functions);
|
|
438
|
+
if (stepResult.status === "indeterminate") return stepResult;
|
|
439
|
+
runningAccumulator = stepResult.value;
|
|
440
|
+
}
|
|
441
|
+
return require_evaluation.definite(runningAccumulator);
|
|
442
|
+
}
|
|
443
|
+
if (includedItems.length === 0) return require_evaluation.indeterminate("domain-error", `'${node.combiner.mode}' has no participating items to seed a running result from`);
|
|
444
|
+
const dominatesOp = node.combiner.mode === "max" ? "gt" : "lt";
|
|
445
|
+
let runningExtremum;
|
|
446
|
+
for (const item of includedItems) {
|
|
447
|
+
const itemResult = await evaluateValueInternal(node.combiner.item, item, resolvers, void 0, functions);
|
|
448
|
+
if (itemResult.status === "indeterminate") return itemResult;
|
|
449
|
+
if (runningExtremum === void 0) {
|
|
450
|
+
runningExtremum = itemResult.value;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const comparison = compareValues(dominatesOp, itemResult.value, runningExtremum);
|
|
454
|
+
if (comparison.status === "indeterminate") return comparison;
|
|
455
|
+
if (comparison.value) runningExtremum = itemResult.value;
|
|
456
|
+
}
|
|
457
|
+
if (runningExtremum === void 0) throw new Error("unreachable: max/min over a non-empty participating list produced no running result");
|
|
458
|
+
return require_evaluation.definite(runningExtremum);
|
|
459
|
+
}
|
|
460
|
+
case "delegate": {
|
|
461
|
+
if (resolvers.resolveDelegate === void 0) return require_evaluation.indeterminate("wrong-type", `no delegate handler registered for external system '${node.system}'`);
|
|
462
|
+
const resolution = await resolvers.resolveDelegate(node.system, node.payload, context);
|
|
463
|
+
if (!resolution.found) return require_evaluation.indeterminate("not-found", `delegate handler for external system '${node.system}' reported no value`);
|
|
464
|
+
return require_evaluation.definite(resolution.value);
|
|
465
|
+
}
|
|
466
|
+
default: throw new Error("unreachable expression node kind");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Builds a bound `{ evaluatePredicate, evaluateValue }` pair over a caller-supplied function registry for `call` nodes -- the registry is bound once, at construction time, unlike `resolvers`, which are supplied fresh to every call. The bare module-level `evaluatePredicate`/`evaluateValue` exports below are `createEvaluator({})`'s output.
|
|
471
|
+
*/
|
|
472
|
+
function createEvaluator({ functions = require_functions.emptyFunctionRegistry }) {
|
|
473
|
+
return {
|
|
474
|
+
evaluatePredicate: async (node, context, resolvers) => evaluatePredicateInternal(node, context, resolvers, void 0, functions),
|
|
475
|
+
evaluateValue: async (node, context, resolvers) => evaluateValueInternal(node, context, resolvers, void 0, functions)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const { evaluatePredicate, evaluateValue } = createEvaluator({});
|
|
479
|
+
//#endregion
|
|
480
|
+
exports.createEvaluator = createEvaluator;
|
|
481
|
+
exports.evaluatePredicate = evaluatePredicate;
|
|
482
|
+
exports.evaluateValue = evaluateValue;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ComputedValue } from "./computed-value.cjs";
|
|
2
|
+
import { ExpressionNode, PredicateNode } from "./tree.cjs";
|
|
3
|
+
import { Evaluation } from "./evaluation.cjs";
|
|
4
|
+
import { FunctionRegistry } from "./functions.cjs";
|
|
5
|
+
import { EvaluationContext, Resolvers } from "./resolvers.cjs";
|
|
6
|
+
//#region src/evaluator.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds a bound `{ evaluatePredicate, evaluateValue }` pair over a caller-supplied function registry for `call` nodes -- the registry is bound once, at construction time, unlike `resolvers`, which are supplied fresh to every call. The bare module-level `evaluatePredicate`/`evaluateValue` exports below are `createEvaluator({})`'s output.
|
|
9
|
+
*/
|
|
10
|
+
declare function createEvaluator({ functions }: {
|
|
11
|
+
functions?: FunctionRegistry;
|
|
12
|
+
}): {
|
|
13
|
+
evaluatePredicate: (node: PredicateNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<boolean>>;
|
|
14
|
+
evaluateValue: (node: ExpressionNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<ComputedValue>>;
|
|
15
|
+
};
|
|
16
|
+
declare const evaluatePredicate: (node: PredicateNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<boolean>>, evaluateValue: (node: ExpressionNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<ComputedValue>>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createEvaluator, evaluatePredicate, evaluateValue };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ComputedValue } from "./computed-value.js";
|
|
2
|
+
import { ExpressionNode, PredicateNode } from "./tree.js";
|
|
3
|
+
import { Evaluation } from "./evaluation.js";
|
|
4
|
+
import { FunctionRegistry } from "./functions.js";
|
|
5
|
+
import { EvaluationContext, Resolvers } from "./resolvers.js";
|
|
6
|
+
//#region src/evaluator.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds a bound `{ evaluatePredicate, evaluateValue }` pair over a caller-supplied function registry for `call` nodes -- the registry is bound once, at construction time, unlike `resolvers`, which are supplied fresh to every call. The bare module-level `evaluatePredicate`/`evaluateValue` exports below are `createEvaluator({})`'s output.
|
|
9
|
+
*/
|
|
10
|
+
declare function createEvaluator({ functions }: {
|
|
11
|
+
functions?: FunctionRegistry;
|
|
12
|
+
}): {
|
|
13
|
+
evaluatePredicate: (node: PredicateNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<boolean>>;
|
|
14
|
+
evaluateValue: (node: ExpressionNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<ComputedValue>>;
|
|
15
|
+
};
|
|
16
|
+
declare const evaluatePredicate: (node: PredicateNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<boolean>>, evaluateValue: (node: ExpressionNode, context: EvaluationContext, resolvers: Readonly<Resolvers>) => Promise<Evaluation<ComputedValue>>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createEvaluator, evaluatePredicate, evaluateValue };
|