@cavelang/solver 0.35.0 → 0.36.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/README.md +2155 -3
- package/dist/src/adapter.d.ts +6 -0
- package/dist/src/adapter.d.ts.map +1 -1
- package/dist/src/adapter.js +3 -0
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/balanced-reduce.d.ts +3 -0
- package/dist/src/balanced-reduce.d.ts.map +1 -0
- package/dist/src/balanced-reduce.js +10 -0
- package/dist/src/balanced-reduce.js.map +1 -0
- package/dist/src/canonical-owned.d.ts +5 -0
- package/dist/src/canonical-owned.d.ts.map +1 -0
- package/dist/src/canonical-owned.js +206 -0
- package/dist/src/canonical-owned.js.map +1 -0
- package/dist/src/canonical.d.ts +3 -2
- package/dist/src/canonical.d.ts.map +1 -1
- package/dist/src/canonical.js +12 -85
- package/dist/src/canonical.js.map +1 -1
- package/dist/src/capability.d.ts.map +1 -1
- package/dist/src/capability.js +43 -33
- package/dist/src/capability.js.map +1 -1
- package/dist/src/clone.d.ts +3 -0
- package/dist/src/clone.d.ts.map +1 -0
- package/dist/src/clone.js +54 -0
- package/dist/src/clone.js.map +1 -0
- package/dist/src/constant-sign.d.ts +5 -0
- package/dist/src/constant-sign.d.ts.map +1 -0
- package/dist/src/constant-sign.js +54 -0
- package/dist/src/constant-sign.js.map +1 -0
- package/dist/src/context.d.ts +6 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +151 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/diagnostic-text.d.ts +3 -0
- package/dist/src/diagnostic-text.d.ts.map +1 -0
- package/dist/src/diagnostic-text.js +5 -0
- package/dist/src/diagnostic-text.js.map +1 -0
- package/dist/src/evaluate-expression.d.ts +4 -0
- package/dist/src/evaluate-expression.d.ts.map +1 -0
- package/dist/src/evaluate-expression.js +24 -0
- package/dist/src/evaluate-expression.js.map +1 -0
- package/dist/src/exact.d.ts +2 -0
- package/dist/src/exact.d.ts.map +1 -1
- package/dist/src/exact.js +125 -28
- package/dist/src/exact.js.map +1 -1
- package/dist/src/explain.d.ts +4 -0
- package/dist/src/explain.d.ts.map +1 -1
- package/dist/src/explain.js +330 -91
- package/dist/src/explain.js.map +1 -1
- package/dist/src/explanation-budget.d.ts +14 -0
- package/dist/src/explanation-budget.d.ts.map +1 -0
- package/dist/src/explanation-budget.js +46 -0
- package/dist/src/explanation-budget.js.map +1 -0
- package/dist/src/expression-key.d.ts +8 -0
- package/dist/src/expression-key.d.ts.map +1 -0
- package/dist/src/expression-key.js +54 -0
- package/dist/src/expression-key.js.map +1 -0
- package/dist/src/fraction-product.d.ts +3 -0
- package/dist/src/fraction-product.d.ts.map +1 -0
- package/dist/src/fraction-product.js +13 -0
- package/dist/src/fraction-product.js.map +1 -0
- package/dist/src/fraction-sum.d.ts +3 -0
- package/dist/src/fraction-sum.d.ts.map +1 -0
- package/dist/src/fraction-sum.js +23 -0
- package/dist/src/fraction-sum.js.map +1 -0
- package/dist/src/integer-gcd.d.ts +3 -0
- package/dist/src/integer-gcd.d.ts.map +1 -0
- package/dist/src/integer-gcd.js +50 -0
- package/dist/src/integer-gcd.js.map +1 -0
- package/dist/src/linear.d.ts +2 -1
- package/dist/src/linear.d.ts.map +1 -1
- package/dist/src/linear.js +85 -17
- package/dist/src/linear.js.map +1 -1
- package/dist/src/numeric-size.d.ts +2 -0
- package/dist/src/numeric-size.d.ts.map +1 -0
- package/dist/src/numeric-size.js +33 -0
- package/dist/src/numeric-size.js.map +1 -0
- package/dist/src/options.d.ts +4 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +18 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/result.d.ts +4 -0
- package/dist/src/result.d.ts.map +1 -0
- package/dist/src/result.js +29 -0
- package/dist/src/result.js.map +1 -0
- package/dist/src/solve.d.ts.map +1 -1
- package/dist/src/solve.js +34 -3
- package/dist/src/solve.js.map +1 -1
- package/dist/src/validate.d.ts +5 -0
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +323 -65
- package/dist/src/validate.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +98 -26
- package/dist/src/workflow.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +9 -0
- package/src/balanced-reduce.ts +9 -0
- package/src/canonical-owned.ts +186 -0
- package/src/canonical.ts +14 -90
- package/src/capability.ts +41 -30
- package/src/clone.ts +47 -0
- package/src/constant-sign.ts +59 -0
- package/src/context.ts +114 -0
- package/src/diagnostic-text.ts +4 -0
- package/src/evaluate-expression.ts +21 -0
- package/src/exact.ts +122 -27
- package/src/explain.ts +342 -94
- package/src/explanation-budget.ts +47 -0
- package/src/expression-key.ts +45 -0
- package/src/fraction-product.ts +10 -0
- package/src/fraction-sum.ts +20 -0
- package/src/integer-gcd.ts +47 -0
- package/src/linear.ts +82 -16
- package/src/numeric-size.ts +26 -0
- package/src/options.ts +18 -0
- package/src/result.ts +28 -0
- package/src/solve.ts +34 -3
- package/src/validate.ts +312 -66
- package/src/workflow.ts +92 -26
package/dist/src/validate.js
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
|
+
import { diagnosticText } from "./diagnostic-text.js";
|
|
1
2
|
import { defaultLimits } from "./adapter.js";
|
|
2
3
|
import * as Exact from "./exact.js";
|
|
4
|
+
import { numericDigits } from "./numeric-size.js";
|
|
3
5
|
import { schema } from "./model.js";
|
|
6
|
+
export { validateContext as explanationContext } from "./context.js";
|
|
7
|
+
/** Validate a portable unknown-outcome reason without changing its contents. */
|
|
8
|
+
export const unknownReason = (value) => {
|
|
9
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
10
|
+
throw new TypeError('unknown solver reason must be an object');
|
|
11
|
+
}
|
|
12
|
+
const { kind, message, limit } = value;
|
|
13
|
+
if (typeof kind !== 'string' || !['timeout', 'resource-limit', 'cancelled', 'backend-error', 'indeterminate'].includes(kind)) {
|
|
14
|
+
throw new TypeError('unknown solver reason kind must be timeout, resource-limit, cancelled, backend-error or indeterminate');
|
|
15
|
+
}
|
|
16
|
+
if (typeof message !== 'string')
|
|
17
|
+
throw new TypeError('unknown solver reason message must be a string');
|
|
18
|
+
if (limit !== undefined && (typeof limit !== 'string' || !Object.hasOwn(defaultLimits, limit))) {
|
|
19
|
+
throw new TypeError('unknown solver reason limit must name a supported solver limit');
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** Validate metadata shared by adapter results and explanation runs. */
|
|
23
|
+
export const resultMetadata = (value) => {
|
|
24
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
25
|
+
throw new TypeError('solver result metadata must be an object');
|
|
26
|
+
}
|
|
27
|
+
const { backend, elapsedMs, diagnostics } = value;
|
|
28
|
+
if (backend === null || typeof backend !== 'object' || Array.isArray(backend)) {
|
|
29
|
+
throw new TypeError('solver backend must be an object');
|
|
30
|
+
}
|
|
31
|
+
const { name, version } = backend;
|
|
32
|
+
if (typeof name !== 'string' || name === '')
|
|
33
|
+
throw new TypeError('solver backend name must be a non-empty string');
|
|
34
|
+
if (typeof version !== 'string' || version === '')
|
|
35
|
+
throw new TypeError('solver backend version must be a non-empty string');
|
|
36
|
+
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
37
|
+
throw new TypeError('solver elapsedMs must be a finite non-negative number');
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(diagnostics))
|
|
40
|
+
throw new TypeError('solver diagnostics must be an array');
|
|
41
|
+
for (let index = 0; index < diagnostics.length; index++) {
|
|
42
|
+
const entry = diagnostics[index];
|
|
43
|
+
if (!Object.hasOwn(diagnostics, index) || entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
44
|
+
throw new TypeError(`solver diagnostics[${index}] must be an object`);
|
|
45
|
+
}
|
|
46
|
+
const { level, code, message } = entry;
|
|
47
|
+
if (level !== 'info' && level !== 'warning' && level !== 'error') {
|
|
48
|
+
throw new TypeError(`solver diagnostics[${index}].level must be info, warning or error`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof code !== 'string' || typeof message !== 'string') {
|
|
51
|
+
throw new TypeError(`solver diagnostics[${index}] code and message must be strings`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
4
55
|
export class ModelValidationError extends Error {
|
|
5
56
|
problems;
|
|
6
57
|
constructor(problems) {
|
|
@@ -25,7 +76,8 @@ const idPattern = /^[A-Za-z][A-Za-z0-9._:/-]*$/;
|
|
|
25
76
|
const numeric = (sort) => sort.kind === 'int' || sort.kind === 'real';
|
|
26
77
|
const same = (left, right) => left.kind === right.kind && (left.kind !== 'enum' || (right.kind === 'enum' && left.domain === right.domain));
|
|
27
78
|
const compatible = (left, right) => same(left, right) || (numeric(left) && numeric(right));
|
|
28
|
-
const
|
|
79
|
+
const captureRational = (value) => value !== null && typeof value === 'object'
|
|
80
|
+
? { numerator: value.numerator, denominator: value.denominator } : value;
|
|
29
81
|
const pushExactProblem = (problems, path, run) => {
|
|
30
82
|
try {
|
|
31
83
|
run();
|
|
@@ -34,19 +86,41 @@ const pushExactProblem = (problems, path, run) => {
|
|
|
34
86
|
problems.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
87
|
}
|
|
36
88
|
};
|
|
89
|
+
/** Describe malformed fields without traversing caller objects or invoking JSON hooks. */
|
|
90
|
+
const diagnosticValue = (value) => {
|
|
91
|
+
if (typeof value === 'string')
|
|
92
|
+
return diagnosticText(value);
|
|
93
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
94
|
+
return JSON.stringify(value);
|
|
95
|
+
}
|
|
96
|
+
return value === undefined ? 'undefined' : `<${typeof value}>`;
|
|
97
|
+
};
|
|
98
|
+
const diagnosticSort = (kind) => typeof kind === 'string' ? diagnosticText(kind, false) : diagnosticValue(kind);
|
|
37
99
|
const requireId = (id, path, problems) => {
|
|
38
|
-
if (
|
|
100
|
+
if (typeof id !== 'string' || id.match(idPattern)?.[0] !== id) {
|
|
39
101
|
problems.push(`${path} must match ${String(idPattern)}`);
|
|
40
102
|
}
|
|
41
103
|
};
|
|
42
104
|
const provenance = (value, path, problems) => {
|
|
105
|
+
const description = value.description;
|
|
106
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
107
|
+
problems.push(`${path}.description must be a string`);
|
|
108
|
+
}
|
|
43
109
|
if (value.declaration !== undefined) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
110
|
+
const declaration = value.declaration;
|
|
111
|
+
if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
|
|
112
|
+
problems.push(`${path}.declaration must be an object`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (typeof declaration.uri !== 'string')
|
|
116
|
+
problems.push(`${path}.declaration.uri must be a string`);
|
|
117
|
+
else if (declaration.uri.trim() === '')
|
|
118
|
+
problems.push(`${path}.declaration.uri must not be empty`);
|
|
119
|
+
for (const position of ['line', 'column']) {
|
|
120
|
+
const at = declaration[position];
|
|
121
|
+
if (at !== undefined && (!Number.isSafeInteger(at) || at <= 0)) {
|
|
122
|
+
problems.push(`${path}.declaration.${position} must be a positive safe integer`);
|
|
123
|
+
}
|
|
50
124
|
}
|
|
51
125
|
}
|
|
52
126
|
}
|
|
@@ -56,58 +130,159 @@ const provenance = (value, path, problems) => {
|
|
|
56
130
|
]) {
|
|
57
131
|
if (ids === undefined)
|
|
58
132
|
continue;
|
|
59
|
-
if (
|
|
133
|
+
if (!Array.isArray(ids)) {
|
|
134
|
+
problems.push(`${path}.${name} must be an array`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
let duplicate = false;
|
|
139
|
+
for (let index = 0; index < ids.length; index++) {
|
|
140
|
+
const id = Object.hasOwn(ids, index) ? ids[index] : undefined;
|
|
141
|
+
if (typeof id !== 'string')
|
|
142
|
+
problems.push(`${path}.${name}[${index}] must be a string`);
|
|
143
|
+
else {
|
|
144
|
+
if (id.trim() === '')
|
|
145
|
+
problems.push(`${path}.${name}[${index}] must not be empty`);
|
|
146
|
+
if (seen.has(id))
|
|
147
|
+
duplicate = true;
|
|
148
|
+
seen.add(id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (duplicate)
|
|
60
152
|
problems.push(`${path}.${name} contains duplicate identifiers`);
|
|
61
|
-
ids.forEach((id, index) => {
|
|
62
|
-
if (id.trim() === '')
|
|
63
|
-
problems.push(`${path}.${name}[${index}] must not be empty`);
|
|
64
|
-
});
|
|
65
153
|
}
|
|
66
154
|
};
|
|
155
|
+
/** Traverse caller arrays without dispatching through overridden array methods. */
|
|
156
|
+
const eachIndexed = (values, visit) => {
|
|
157
|
+
const length = values.length;
|
|
158
|
+
for (let index = 0; index < length; index++)
|
|
159
|
+
visit(values[index], index);
|
|
160
|
+
};
|
|
161
|
+
const mapIndexed = (values, visit) => {
|
|
162
|
+
const result = [];
|
|
163
|
+
eachIndexed(values, (value, index) => result.push(visit(value, index)));
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
67
166
|
const duplicates = (ids, path, problems) => {
|
|
68
167
|
const seen = new Set();
|
|
69
168
|
for (const id of ids) {
|
|
70
169
|
if (seen.has(id)) {
|
|
71
|
-
problems.push(`${path} contains duplicate identifier ${
|
|
170
|
+
problems.push(`${path} contains duplicate identifier ${diagnosticValue(id)}`);
|
|
72
171
|
}
|
|
73
172
|
seen.add(id);
|
|
74
173
|
}
|
|
75
174
|
};
|
|
175
|
+
function* children(expression) {
|
|
176
|
+
switch (expression.kind) {
|
|
177
|
+
case 'not':
|
|
178
|
+
case 'negate':
|
|
179
|
+
yield { value: expression.value, suffix: 'value' };
|
|
180
|
+
return;
|
|
181
|
+
case 'and':
|
|
182
|
+
case 'or':
|
|
183
|
+
case 'add':
|
|
184
|
+
case 'multiply':
|
|
185
|
+
for (let index = 0; index < expression.operands.length; index++) {
|
|
186
|
+
yield { value: Object.hasOwn(expression.operands, index) ? expression.operands[index] : undefined, suffix: `operands[${index}]` };
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
case 'if':
|
|
190
|
+
yield { value: expression.condition, suffix: 'condition' };
|
|
191
|
+
yield { value: expression.then, suffix: 'then' };
|
|
192
|
+
yield { value: expression.else, suffix: 'else' };
|
|
193
|
+
return;
|
|
194
|
+
case 'implies':
|
|
195
|
+
case 'eq':
|
|
196
|
+
case 'neq':
|
|
197
|
+
case 'lt':
|
|
198
|
+
case 'lte':
|
|
199
|
+
case 'gt':
|
|
200
|
+
case 'gte':
|
|
201
|
+
case 'subtract':
|
|
202
|
+
case 'divide':
|
|
203
|
+
yield { value: expression.left, suffix: 'left' };
|
|
204
|
+
yield { value: expression.right, suffix: 'right' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
76
207
|
const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
208
|
+
const frames = [];
|
|
209
|
+
const resolved = new WeakMap();
|
|
210
|
+
const enter = (value, at, level) => {
|
|
211
|
+
walk.nodes += 1;
|
|
212
|
+
walk.depth = Math.max(walk.depth, level);
|
|
213
|
+
if (walk.nodes > walk.limits.maxExpressionNodes) {
|
|
214
|
+
throw new ModelLimitError('maxExpressionNodes', walk.nodes, walk.limits.maxExpressionNodes);
|
|
215
|
+
}
|
|
216
|
+
if (level > walk.limits.maxExpressionDepth) {
|
|
217
|
+
throw new ModelLimitError('maxExpressionDepth', level, walk.limits.maxExpressionDepth);
|
|
218
|
+
}
|
|
219
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
220
|
+
throw new ModelValidationError([`${at} must be an expression object`]);
|
|
221
|
+
}
|
|
222
|
+
if ((value.kind === 'and' || value.kind === 'or' || value.kind === 'add' || value.kind === 'multiply') && !Array.isArray(value.operands)) {
|
|
223
|
+
throw new ModelValidationError([`${at}.operands must be an array`]);
|
|
224
|
+
}
|
|
225
|
+
frames.push({ expression: value, path: at, depth: level, children: children(value) });
|
|
226
|
+
};
|
|
227
|
+
enter(expression, path, depth);
|
|
228
|
+
while (frames.length > 0) {
|
|
229
|
+
const frame = frames[frames.length - 1];
|
|
230
|
+
const next = frame.children.next();
|
|
231
|
+
if (!next.done) {
|
|
232
|
+
enter(next.value.value, `${frame.path}.${next.value.suffix}`, frame.depth + 1);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
resolved.set(frame.expression, inferNode(frame.expression, frame.path, variables, domains, problems, resolved, walk));
|
|
236
|
+
frames.pop();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return resolved.get(expression);
|
|
240
|
+
};
|
|
241
|
+
const inferNode = (expression, path, variables, domains, problems, resolved, walk) => {
|
|
242
|
+
const child = (value, _suffix) => resolved.get(value);
|
|
80
243
|
const expectBool = (sort, at) => {
|
|
81
244
|
if (sort.kind !== 'bool')
|
|
82
|
-
problems.push(`${at} must be boolean, received ${sort.kind}`);
|
|
245
|
+
problems.push(`${at} must be boolean, received ${diagnosticSort(sort.kind)}`);
|
|
83
246
|
};
|
|
84
247
|
const expectNumeric = (sort, at) => {
|
|
85
248
|
if (!numeric(sort))
|
|
86
|
-
problems.push(`${at} must be numeric, received ${sort.kind}`);
|
|
249
|
+
problems.push(`${at} must be numeric, received ${diagnosticSort(sort.kind)}`);
|
|
87
250
|
};
|
|
88
251
|
switch (expression.kind) {
|
|
89
252
|
case 'literal':
|
|
90
253
|
switch (expression.sort) {
|
|
91
|
-
case 'bool':
|
|
92
|
-
|
|
93
|
-
|
|
254
|
+
case 'bool':
|
|
255
|
+
if (typeof expression.value !== 'boolean')
|
|
256
|
+
problems.push(`${path}.value must be a Boolean`);
|
|
257
|
+
return { kind: 'bool' };
|
|
258
|
+
case 'int': {
|
|
259
|
+
const value = expression.value;
|
|
260
|
+
walk.charge(value, 'int');
|
|
261
|
+
pushExactProblem(problems, path, () => walk.zeroLiterals.set(expression, Exact.integer(value) === 0n));
|
|
94
262
|
return { kind: 'int' };
|
|
95
|
-
|
|
96
|
-
|
|
263
|
+
}
|
|
264
|
+
case 'real': {
|
|
265
|
+
const value = captureRational(expression.value);
|
|
266
|
+
walk.charge(value, 'real');
|
|
267
|
+
pushExactProblem(problems, path, () => walk.zeroLiterals.set(expression, Exact.rational(value).numerator === '0'));
|
|
97
268
|
return { kind: 'real' };
|
|
269
|
+
}
|
|
98
270
|
case 'enum': {
|
|
99
271
|
const values = domains.get(expression.domain);
|
|
100
272
|
if (values === undefined)
|
|
101
|
-
problems.push(`${path} references unknown enum domain ${
|
|
102
|
-
else if (!values.
|
|
103
|
-
problems.push(`${path} value ${
|
|
273
|
+
problems.push(`${path} references unknown enum domain ${diagnosticValue(expression.domain)}`);
|
|
274
|
+
else if (!values.has(expression.value))
|
|
275
|
+
problems.push(`${path} value ${diagnosticValue(expression.value)} is outside enum domain ${diagnosticValue(expression.domain)}`);
|
|
104
276
|
return { kind: 'enum', domain: expression.domain };
|
|
105
277
|
}
|
|
278
|
+
default:
|
|
279
|
+
problems.push(`${path} uses an unsupported literal sort`);
|
|
280
|
+
return { kind: 'bool' };
|
|
106
281
|
}
|
|
107
282
|
case 'variable': {
|
|
108
283
|
const variable = variables.get(expression.id);
|
|
109
284
|
if (variable === undefined) {
|
|
110
|
-
problems.push(`${path} references unknown variable ${
|
|
285
|
+
problems.push(`${path} references unknown variable ${diagnosticValue(expression.id)}`);
|
|
111
286
|
return { kind: 'bool' };
|
|
112
287
|
}
|
|
113
288
|
return variable.sort === 'enum' ? { kind: 'enum', domain: variable.domain } : { kind: variable.sort };
|
|
@@ -120,7 +295,7 @@ const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
|
120
295
|
case 'or': {
|
|
121
296
|
if (expression.operands.length < 2)
|
|
122
297
|
problems.push(`${path}.${expression.kind} requires at least two operands`);
|
|
123
|
-
expression.operands
|
|
298
|
+
eachIndexed(expression.operands, (operand, index) => expectBool(child(operand, `operands[${index}]`), `${path}.operands[${index}]`));
|
|
124
299
|
return { kind: 'bool' };
|
|
125
300
|
}
|
|
126
301
|
case 'implies': {
|
|
@@ -133,7 +308,7 @@ const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
|
133
308
|
const left = child(expression.left, 'left');
|
|
134
309
|
const right = child(expression.right, 'right');
|
|
135
310
|
if (!compatible(left, right))
|
|
136
|
-
problems.push(`${path} compares incompatible ${left.kind} and ${right.kind} expressions`);
|
|
311
|
+
problems.push(`${path} compares incompatible ${diagnosticSort(left.kind)} and ${diagnosticSort(right.kind)} expressions`);
|
|
137
312
|
return { kind: 'bool' };
|
|
138
313
|
}
|
|
139
314
|
case 'lt':
|
|
@@ -148,7 +323,7 @@ const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
|
148
323
|
case 'multiply': {
|
|
149
324
|
if (expression.operands.length < 2)
|
|
150
325
|
problems.push(`${path}.${expression.kind} requires at least two operands`);
|
|
151
|
-
const sorts = expression.operands
|
|
326
|
+
const sorts = mapIndexed(expression.operands, (operand, index) => child(operand, `operands[${index}]`));
|
|
152
327
|
sorts.forEach((sort, index) => expectNumeric(sort, `${path}.operands[${index}]`));
|
|
153
328
|
return { kind: sorts.some(sort => sort.kind === 'real') ? 'real' : 'int' };
|
|
154
329
|
}
|
|
@@ -165,7 +340,7 @@ const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
|
165
340
|
const divisor = expression.right;
|
|
166
341
|
if (divisor.kind === 'literal' && (divisor.sort === 'int' || divisor.sort === 'real')) {
|
|
167
342
|
pushExactProblem(problems, `${path}.right`, () => {
|
|
168
|
-
const zero =
|
|
343
|
+
const zero = walk.zeroLiterals.get(divisor);
|
|
169
344
|
if (zero)
|
|
170
345
|
throw new TypeError('literal divisor must not be zero');
|
|
171
346
|
});
|
|
@@ -182,21 +357,29 @@ const infer = (expression, path, variables, domains, problems, walk, depth) => {
|
|
|
182
357
|
const thenSort = child(expression.then, 'then');
|
|
183
358
|
const elseSort = child(expression.else, 'else');
|
|
184
359
|
if (!compatible(thenSort, elseSort))
|
|
185
|
-
problems.push(`${path} branches have incompatible ${thenSort.kind} and ${elseSort.kind} sorts`);
|
|
360
|
+
problems.push(`${path} branches have incompatible ${diagnosticSort(thenSort.kind)} and ${diagnosticSort(elseSort.kind)} sorts`);
|
|
186
361
|
return numeric(thenSort) && numeric(elseSort) && (thenSort.kind === 'real' || elseSort.kind === 'real')
|
|
187
362
|
? { kind: 'real' }
|
|
188
363
|
: thenSort;
|
|
189
364
|
}
|
|
190
365
|
default:
|
|
191
|
-
problems.push(`${path} uses unsupported expression kind ${
|
|
366
|
+
problems.push(`${path} uses unsupported expression kind ${diagnosticValue(expression.kind)}`);
|
|
192
367
|
return { kind: 'bool' };
|
|
193
368
|
}
|
|
194
369
|
};
|
|
195
370
|
export const mergeLimits = (input = {}) => {
|
|
196
|
-
|
|
371
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
372
|
+
throw new TypeError('solver limits must be an object');
|
|
373
|
+
}
|
|
374
|
+
for (const name of Object.keys(input)) {
|
|
375
|
+
if (!Object.hasOwn(defaultLimits, name))
|
|
376
|
+
throw new TypeError(`unknown solver limit ${diagnosticText(name)}`);
|
|
377
|
+
}
|
|
378
|
+
const limits = Object.fromEntries(Object.entries(defaultLimits).map(([name, fallback]) => [name, name in input ? input[name] : fallback]));
|
|
197
379
|
for (const [name, value] of Object.entries(limits)) {
|
|
198
380
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
199
|
-
|
|
381
|
+
const received = typeof value === 'object' || typeof value === 'function' ? typeof value : diagnosticText(String(value), false);
|
|
382
|
+
throw new TypeError(`solver limit ${name} must be a positive safe integer, received ${received}`);
|
|
200
383
|
}
|
|
201
384
|
}
|
|
202
385
|
return limits;
|
|
@@ -219,47 +402,117 @@ const enforce = (stats, limits) => {
|
|
|
219
402
|
export const model = (input, limitInput = {}) => {
|
|
220
403
|
const limits = mergeLimits(limitInput);
|
|
221
404
|
const problems = [];
|
|
405
|
+
let numericTotal = 0;
|
|
406
|
+
const charge = (value, sort) => {
|
|
407
|
+
numericTotal += numericDigits(value, sort);
|
|
408
|
+
if (numericTotal > limits.maxNumericDigits) {
|
|
409
|
+
throw new ModelLimitError('maxNumericDigits', numericTotal, limits.maxNumericDigits);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
413
|
+
throw new ModelValidationError(['model must be an object']);
|
|
414
|
+
}
|
|
415
|
+
for (const key of ['variables', 'constraints', 'enums', 'softConstraints', 'objectives']) {
|
|
416
|
+
const declarations = input[key];
|
|
417
|
+
if (declarations === undefined && key !== 'variables' && key !== 'constraints')
|
|
418
|
+
continue;
|
|
419
|
+
if (!Array.isArray(declarations)) {
|
|
420
|
+
problems.push(`${key} must be an array`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (problems.length > 0)
|
|
424
|
+
throw new ModelValidationError(problems);
|
|
425
|
+
enforce({
|
|
426
|
+
variables: input.variables.length,
|
|
427
|
+
constraints: input.constraints.length + (input.softConstraints?.length ?? 0),
|
|
428
|
+
objectives: input.objectives?.length ?? 0,
|
|
429
|
+
enumValues: 0, expressionNodes: 0, expressionDepth: 0,
|
|
430
|
+
}, limits);
|
|
431
|
+
for (const key of ['variables', 'constraints', 'enums', 'softConstraints', 'objectives']) {
|
|
432
|
+
const declarations = input[key];
|
|
433
|
+
if (declarations === undefined)
|
|
434
|
+
continue;
|
|
435
|
+
for (let index = 0; index < declarations.length; index++) {
|
|
436
|
+
const declaration = Object.hasOwn(declarations, index) ? declarations[index] : undefined;
|
|
437
|
+
if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
|
|
438
|
+
problems.push(`${key}[${index}] must be a declaration object`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (problems.length > 0)
|
|
443
|
+
throw new ModelValidationError(problems);
|
|
222
444
|
if (input.schema !== schema)
|
|
223
445
|
problems.push(`schema must be ${JSON.stringify(schema)}`);
|
|
224
446
|
const domains = new Map();
|
|
225
447
|
const enumDeclarations = input.enums ?? [];
|
|
226
|
-
|
|
227
|
-
|
|
448
|
+
let enumValues = 0;
|
|
449
|
+
duplicates(mapIndexed(enumDeclarations, domain => domain.id), 'enums', problems);
|
|
450
|
+
eachIndexed(enumDeclarations, (domain, index) => {
|
|
228
451
|
requireId(domain.id, `enums[${index}].id`, problems);
|
|
229
452
|
provenance(domain, `enums[${index}]`, problems);
|
|
230
|
-
|
|
453
|
+
const values = Array.isArray(domain.values) ? domain.values : [];
|
|
454
|
+
if (!Array.isArray(domain.values))
|
|
455
|
+
problems.push(`enums[${index}].values must be an array`);
|
|
456
|
+
enumValues += values.length;
|
|
457
|
+
if (enumValues > limits.maxEnumValues)
|
|
458
|
+
throw new ModelLimitError('maxEnumValues', enumValues, limits.maxEnumValues);
|
|
459
|
+
if (values.length === 0)
|
|
231
460
|
problems.push(`enums[${index}] must contain at least one value`);
|
|
232
|
-
|
|
461
|
+
const members = new Set();
|
|
462
|
+
let duplicate = false;
|
|
463
|
+
for (let member = 0; member < values.length; member++) {
|
|
464
|
+
const value = Object.hasOwn(values, member) ? values[member] : undefined;
|
|
465
|
+
if (typeof value !== 'string')
|
|
466
|
+
problems.push(`enums[${index}].values[${member}] must be a string`);
|
|
467
|
+
else {
|
|
468
|
+
if (members.has(value))
|
|
469
|
+
duplicate = true;
|
|
470
|
+
members.add(value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (duplicate)
|
|
233
474
|
problems.push(`enums[${index}] contains duplicate values`);
|
|
234
|
-
domains.set(domain.id,
|
|
475
|
+
domains.set(domain.id, members);
|
|
235
476
|
});
|
|
236
477
|
const variables = new Map();
|
|
237
|
-
duplicates(input.variables
|
|
238
|
-
input.variables
|
|
478
|
+
duplicates(mapIndexed(input.variables, variable => variable.id), 'variables', problems);
|
|
479
|
+
eachIndexed(input.variables, (variable, index) => {
|
|
239
480
|
requireId(variable.id, `variables[${index}].id`, problems);
|
|
481
|
+
if (!['bool', 'int', 'real', 'enum'].includes(variable.sort))
|
|
482
|
+
problems.push(`variables[${index}] uses an unsupported sort`);
|
|
240
483
|
provenance(variable, `variables[${index}]`, problems);
|
|
241
484
|
variables.set(variable.id, variable);
|
|
242
485
|
if (variable.sort === 'enum' && !domains.has(variable.domain)) {
|
|
243
|
-
problems.push(`variables[${index}] references unknown enum domain ${
|
|
486
|
+
problems.push(`variables[${index}] references unknown enum domain ${diagnosticValue(variable.domain)}`);
|
|
244
487
|
}
|
|
245
488
|
if (variable.sort === 'int') {
|
|
246
|
-
|
|
247
|
-
|
|
489
|
+
const min = variable.min, max = variable.max;
|
|
490
|
+
charge(min, 'int');
|
|
491
|
+
charge(max, 'int');
|
|
492
|
+
pushExactProblem(problems, `variables[${index}].min`, () => Exact.integer(min));
|
|
493
|
+
pushExactProblem(problems, `variables[${index}].max`, () => Exact.integer(max));
|
|
248
494
|
try {
|
|
249
|
-
if (Exact.compare(
|
|
495
|
+
if (Exact.compare(String(Exact.integer(min)), String(Exact.integer(max))) > 0) {
|
|
250
496
|
problems.push(`variables[${index}] has min greater than max`);
|
|
251
497
|
}
|
|
252
498
|
}
|
|
253
499
|
catch { /* exact-value problems are already reported above */ }
|
|
254
500
|
}
|
|
255
501
|
if (variable.sort === 'real') {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
502
|
+
const rawMin = variable.min, rawMax = variable.max;
|
|
503
|
+
const min = rawMin === undefined ? undefined : captureRational(rawMin);
|
|
504
|
+
const max = rawMax === undefined ? undefined : captureRational(rawMax);
|
|
505
|
+
if (min !== undefined)
|
|
506
|
+
charge(min, 'real');
|
|
507
|
+
if (max !== undefined)
|
|
508
|
+
charge(max, 'real');
|
|
509
|
+
if (min !== undefined)
|
|
510
|
+
pushExactProblem(problems, `variables[${index}].min`, () => Exact.rational(min));
|
|
511
|
+
if (max !== undefined)
|
|
512
|
+
pushExactProblem(problems, `variables[${index}].max`, () => Exact.rational(max));
|
|
513
|
+
if (min !== undefined && max !== undefined) {
|
|
261
514
|
try {
|
|
262
|
-
if (Exact.compare(
|
|
515
|
+
if (Exact.compare(min, max) > 0)
|
|
263
516
|
problems.push(`variables[${index}] has min greater than max`);
|
|
264
517
|
}
|
|
265
518
|
catch { /* exact-value problems are already reported above */ }
|
|
@@ -267,35 +520,40 @@ export const model = (input, limitInput = {}) => {
|
|
|
267
520
|
}
|
|
268
521
|
});
|
|
269
522
|
duplicates([
|
|
270
|
-
...input.constraints
|
|
271
|
-
...(input.softConstraints ?? []
|
|
523
|
+
...mapIndexed(input.constraints, constraint => constraint.id),
|
|
524
|
+
...mapIndexed(input.softConstraints ?? [], constraint => constraint.id)
|
|
272
525
|
], 'constraints', problems);
|
|
273
|
-
duplicates((input.objectives ?? []
|
|
274
|
-
const walk = { nodes: 0, depth: 0 };
|
|
275
|
-
input.constraints
|
|
526
|
+
duplicates(mapIndexed(input.objectives ?? [], objective => objective.id), 'objectives', problems);
|
|
527
|
+
const walk = { nodes: 0, depth: 0, limits, charge, zeroLiterals: new WeakMap() };
|
|
528
|
+
eachIndexed(input.constraints, (constraint, index) => {
|
|
276
529
|
requireId(constraint.id, `constraints[${index}].id`, problems);
|
|
277
530
|
provenance(constraint, `constraints[${index}]`, problems);
|
|
278
531
|
const sort = infer(constraint.expression, `constraints[${index}].expression`, variables, domains, problems, walk, 1);
|
|
279
532
|
if (sort.kind !== 'bool')
|
|
280
|
-
problems.push(`constraints[${index}].expression must be boolean, received ${sort.kind}`);
|
|
533
|
+
problems.push(`constraints[${index}].expression must be boolean, received ${diagnosticSort(sort.kind)}`);
|
|
281
534
|
});
|
|
282
|
-
(input.softConstraints ?? []
|
|
535
|
+
eachIndexed(input.softConstraints ?? [], (constraint, index) => {
|
|
283
536
|
requireId(constraint.id, `softConstraints[${index}].id`, problems);
|
|
284
537
|
provenance(constraint, `softConstraints[${index}]`, problems);
|
|
285
538
|
const sort = infer(constraint.expression, `softConstraints[${index}].expression`, variables, domains, problems, walk, 1);
|
|
286
539
|
if (sort.kind !== 'bool')
|
|
287
|
-
problems.push(`softConstraints[${index}].expression must be boolean, received ${sort.kind}`);
|
|
540
|
+
problems.push(`softConstraints[${index}].expression must be boolean, received ${diagnosticSort(sort.kind)}`);
|
|
541
|
+
const weight = captureRational(constraint.weight);
|
|
542
|
+
charge(weight, 'real');
|
|
288
543
|
pushExactProblem(problems, `softConstraints[${index}].weight`, () => {
|
|
289
|
-
if (Exact.compare(
|
|
544
|
+
if (Exact.compare(weight, '0') <= 0)
|
|
290
545
|
throw new TypeError('weight must be greater than zero');
|
|
291
546
|
});
|
|
292
547
|
});
|
|
293
|
-
(input.objectives ?? []
|
|
548
|
+
eachIndexed(input.objectives ?? [], (objective, index) => {
|
|
294
549
|
requireId(objective.id, `objectives[${index}].id`, problems);
|
|
550
|
+
if (objective.direction !== 'minimize' && objective.direction !== 'maximize') {
|
|
551
|
+
problems.push(`objectives[${index}].direction must be minimize or maximize`);
|
|
552
|
+
}
|
|
295
553
|
provenance(objective, `objectives[${index}]`, problems);
|
|
296
554
|
const sort = infer(objective.expression, `objectives[${index}].expression`, variables, domains, problems, walk, 1);
|
|
297
555
|
if (!numeric(sort))
|
|
298
|
-
problems.push(`objectives[${index}].expression must be numeric, received ${sort.kind}`);
|
|
556
|
+
problems.push(`objectives[${index}].expression must be numeric, received ${diagnosticSort(sort.kind)}`);
|
|
299
557
|
});
|
|
300
558
|
if (problems.length > 0)
|
|
301
559
|
throw new ModelValidationError(problems);
|
|
@@ -303,7 +561,7 @@ export const model = (input, limitInput = {}) => {
|
|
|
303
561
|
variables: input.variables.length,
|
|
304
562
|
constraints: input.constraints.length + (input.softConstraints?.length ?? 0),
|
|
305
563
|
objectives: input.objectives?.length ?? 0,
|
|
306
|
-
enumValues
|
|
564
|
+
enumValues,
|
|
307
565
|
expressionNodes: walk.nodes,
|
|
308
566
|
expressionDepth: walk.depth
|
|
309
567
|
};
|