@actuarial-ts/core 0.10.0 → 0.11.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 +9 -7
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +64 -5
- package/dist/canonical.js.map +1 -1
- package/dist/customizationContracts.d.ts +10 -1
- package/dist/customizationContracts.d.ts.map +1 -1
- package/dist/descriptiveStatistics.js +1 -1
- package/dist/descriptiveStatistics.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +26 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +125 -11
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticAuditText.d.ts +11 -0
- package/dist/diagnosticAuditText.d.ts.map +1 -0
- package/dist/diagnosticAuditText.js +179 -0
- package/dist/diagnosticAuditText.js.map +1 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +71 -12
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticFreeze.d.ts +15 -1
- package/dist/diagnosticFreeze.d.ts.map +1 -1
- package/dist/diagnosticFreeze.js +82 -31
- package/dist/diagnosticFreeze.js.map +1 -1
- package/dist/diagnosticKeys.d.ts +9 -0
- package/dist/diagnosticKeys.d.ts.map +1 -0
- package/dist/diagnosticKeys.js +20 -0
- package/dist/diagnosticKeys.js.map +1 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -1
- package/dist/diagnosticOrdering.js +9 -2
- package/dist/diagnosticOrdering.js.map +1 -1
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +19 -1
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +19 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +544 -231
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +419 -186
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +6 -1
- package/dist/diagnosticReviewSources.d.ts.map +1 -1
- package/dist/diagnosticReviewSources.js +59 -14
- package/dist/diagnosticReviewSources.js.map +1 -1
- package/dist/diagnosticReviewStore.d.ts.map +1 -1
- package/dist/diagnosticReviewStore.js +48 -7
- package/dist/diagnosticReviewStore.js.map +1 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +112 -37
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts.map +1 -1
- package/dist/diagnosticRuntime.js +11 -1
- package/dist/diagnosticRuntime.js.map +1 -1
- package/dist/diagnosticSourceOrdering.d.ts +27 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -1
- package/dist/diagnosticSourceOrdering.js +98 -12
- package/dist/diagnosticSourceOrdering.js.map +1 -1
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +35 -0
- package/dist/mack.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/canonical.ts +63 -5
- package/src/customizationContracts.ts +10 -1
- package/src/descriptiveStatistics.ts +1 -1
- package/src/diagnosticAggregation.ts +173 -9
- package/src/diagnosticAuditText.ts +187 -0
- package/src/diagnosticFormulas.ts +77 -27
- package/src/diagnosticFreeze.ts +75 -30
- package/src/diagnosticKeys.ts +22 -0
- package/src/diagnosticOrdering.ts +9 -8
- package/src/diagnosticPeriods.ts +21 -1
- package/src/diagnosticPreparation.ts +624 -272
- package/src/diagnosticReview.ts +566 -268
- package/src/diagnosticReviewSources.ts +69 -18
- package/src/diagnosticReviewStore.ts +48 -6
- package/src/diagnosticRunner.ts +133 -54
- package/src/diagnosticRuntime.ts +11 -1
- package/src/diagnosticSourceOrdering.ts +115 -11
- package/src/mack.ts +41 -0
- package/src/types.ts +1 -0
- package/src/version.ts +1 -1
package/src/diagnosticReview.ts
CHANGED
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
getCompiledDiagnosticDefinitionInternals,
|
|
3
3
|
type DiagnosticControlTotalProjection,
|
|
4
4
|
type DiagnosticDeepReadonly,
|
|
5
|
+
type DiagnosticMeasureStats,
|
|
5
6
|
type DiagnosticReviewFilter,
|
|
6
7
|
type DiagnosticReviewRule,
|
|
7
8
|
type DiagnosticSourceLocation,
|
|
8
9
|
} from "./diagnosticDefinitions.js";
|
|
9
10
|
import type { NormalizedDiagnosticReviewFilterIdentity } from "./diagnosticIdentity.js";
|
|
10
11
|
import { canonicalJson } from "./canonical.js";
|
|
12
|
+
import { canonicalScalarArrayJson } from "./diagnosticKeys.js";
|
|
11
13
|
import { createDiagnosticReviewSourcePool } from "./diagnosticReviewSources.js";
|
|
12
14
|
import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
|
|
13
15
|
import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
evaluateDiagnosticMeasureExpression,
|
|
16
18
|
type FinalizedDiagnosticMeasure,
|
|
17
19
|
} from "./diagnosticFormulas.js";
|
|
20
|
+
import { hasDiagnosticOwn } from "./diagnosticRuntime.js";
|
|
18
21
|
import {
|
|
19
22
|
assertPreparedDiagnosticData,
|
|
20
23
|
assertCompactPreparedDiagnosticData,
|
|
@@ -128,6 +131,12 @@ interface EvaluatedOperand {
|
|
|
128
131
|
readonly sources: readonly DiagnosticSourceLocation[];
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
// Shared immutable empties for the overwhelmingly common clean cell: readers
|
|
135
|
+
// only iterate or spread these, and every retained evaluation owns fresh arrays.
|
|
136
|
+
const NO_REASONS: readonly DiagnosticRuleNotEvaluatedReason[] = Object.freeze([]);
|
|
137
|
+
const NO_REVIEW_OVERFLOWS: readonly DiagnosticReviewExpressionOverflow[] = Object.freeze([]);
|
|
138
|
+
const NO_SOURCES: readonly DiagnosticSourceLocation[] = Object.freeze([]);
|
|
139
|
+
|
|
131
140
|
const REASON_ORDER: readonly DiagnosticRuleNotEvaluatedReason[] = [
|
|
132
141
|
"missing",
|
|
133
142
|
"imputed",
|
|
@@ -184,46 +193,66 @@ function expressionLeafPaths(
|
|
|
184
193
|
];
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
// A projected state depends only on its measure's definition and the cell's
|
|
197
|
+
// statistics record, and preparation shares equal statistics records across
|
|
198
|
+
// cells, so one projection per (measure, record) serves every cell holding it.
|
|
199
|
+
// The cache lives in one review invocation, so no projection crosses
|
|
200
|
+
// preparations or definitions. Bounded like the interner.
|
|
201
|
+
const MAX_CACHED_STATES = 100_000;
|
|
202
|
+
type StateCache = Map<string, Map<DiagnosticMeasureStats, FinalizedDiagnosticMeasure>>;
|
|
203
|
+
|
|
187
204
|
function states(
|
|
188
205
|
prepared: PreparedDiagnosticDataContent,
|
|
189
206
|
cell: PreparedDiagnosticData["cells"][number],
|
|
207
|
+
cache: StateCache,
|
|
190
208
|
): Record<string, FinalizedDiagnosticMeasure> {
|
|
191
209
|
const internals = getCompiledDiagnosticDefinitionInternals(
|
|
192
210
|
prepared.definition,
|
|
193
211
|
);
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const result: Record<string, FinalizedDiagnosticMeasure> = {};
|
|
213
|
+
const ids = Object.keys(cell.components);
|
|
214
|
+
let cached = 0;
|
|
215
|
+
for (let index = 0; index < ids.length; index++) {
|
|
216
|
+
const id = ids[index]!;
|
|
217
|
+
const stats = cell.components[id]!;
|
|
218
|
+
let byStats = cache.get(id);
|
|
219
|
+
if (byStats === undefined) {
|
|
220
|
+
byStats = new Map();
|
|
221
|
+
cache.set(id, byStats);
|
|
222
|
+
}
|
|
223
|
+
let state = byStats.get(stats);
|
|
224
|
+
if (state === undefined) {
|
|
196
225
|
const measure = internals.measuresById.get(id)!;
|
|
197
|
-
|
|
226
|
+
let readiness: DiagnosticRuleNotEvaluatedReason[] | undefined;
|
|
198
227
|
if (stats.missing)
|
|
199
|
-
readiness.push(stats.imputedZero ? "imputed" : "missing");
|
|
200
|
-
if (stats.nonFinite) readiness.push("non-finite");
|
|
201
|
-
if (stats.structural) readiness.push("structural-ambiguity");
|
|
228
|
+
(readiness ??= []).push(stats.imputedZero ? "imputed" : "missing");
|
|
229
|
+
if (stats.nonFinite) (readiness ??= []).push("non-finite");
|
|
230
|
+
if (stats.structural) (readiness ??= []).push("structural-ambiguity");
|
|
202
231
|
if (stats.sum === null && !stats.nonFinite && !stats.structural)
|
|
203
|
-
readiness.push("aggregation-overflow");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
(readiness ??= []).push("aggregation-overflow");
|
|
233
|
+
const quantity: Record<string, unknown> = {
|
|
234
|
+
kind: measure.kind,
|
|
235
|
+
unit: measure.unit,
|
|
236
|
+
};
|
|
237
|
+
if (measure.basisId) quantity.basisId = measure.basisId;
|
|
238
|
+
if (measure.countPopulationId)
|
|
239
|
+
quantity.countPopulationId = measure.countPopulationId;
|
|
240
|
+
if (measure.exposureBasisId)
|
|
241
|
+
quantity.exposureBasisId = measure.exposureBasisId;
|
|
242
|
+
quantity.value = stats.value;
|
|
243
|
+
state = {
|
|
244
|
+
quantity: quantity as unknown as FinalizedDiagnosticMeasure["quantity"],
|
|
245
|
+
stats,
|
|
246
|
+
readiness: readiness ?? NO_REASONS,
|
|
247
|
+
// Cell expressions attach complete leaf-source evidence separately.
|
|
248
|
+
// Per-measure source lists here would be discarded and rebuilt.
|
|
249
|
+
};
|
|
250
|
+
cached += byStats.size;
|
|
251
|
+
if (cached < MAX_CACHED_STATES) byStats.set(stats, state);
|
|
252
|
+
}
|
|
253
|
+
result[id] = state;
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
227
256
|
}
|
|
228
257
|
|
|
229
258
|
function coordinate(
|
|
@@ -300,18 +329,32 @@ function cellSourcesForSelection(
|
|
|
300
329
|
}
|
|
301
330
|
|
|
302
331
|
/** Compact-only, invocation-owned caches. Never retain these on a prepared cell. */
|
|
303
|
-
function createCompactCellSourceReader() {
|
|
332
|
+
function createCompactCellSourceReader(cellCount: number) {
|
|
304
333
|
const pool = createDiagnosticReviewSourcePool();
|
|
305
334
|
const addCellSources = (
|
|
306
335
|
target: ReturnType<typeof pool.collection>,
|
|
307
336
|
cell: PreparedDiagnosticData["cells"][number],
|
|
308
337
|
ids: readonly string[],
|
|
309
338
|
): void => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
339
|
+
// Preparation cites one frozen list per source row across its measures;
|
|
340
|
+
// adding the same list again is a no-op, so skip it by identity.
|
|
341
|
+
let previous: readonly DiagnosticSourceLocation[] | undefined;
|
|
342
|
+
for (let position = 0; position < ids.length; position++) {
|
|
343
|
+
const id = ids[position]!;
|
|
344
|
+
const contributions = cell.contributions[id];
|
|
345
|
+
if (contributions !== undefined)
|
|
346
|
+
for (let index = 0; index < contributions.length; index++) {
|
|
347
|
+
const sources = contributions[index]!.sources;
|
|
348
|
+
if (sources === previous) continue;
|
|
349
|
+
previous = sources;
|
|
350
|
+
for (let at = 0; at < sources.length; at++) target.add(sources[at]!, true);
|
|
351
|
+
}
|
|
352
|
+
const blockers = cell.structuralBlockers[id];
|
|
353
|
+
if (blockers !== undefined)
|
|
354
|
+
for (let index = 0; index < blockers.length; index++) {
|
|
355
|
+
const sources = blockers[index]!.sources;
|
|
356
|
+
for (let at = 0; at < sources.length; at++) target.add(sources[at]!, true);
|
|
357
|
+
}
|
|
315
358
|
}
|
|
316
359
|
};
|
|
317
360
|
const forSelection = (
|
|
@@ -324,11 +367,33 @@ function createCompactCellSourceReader() {
|
|
|
324
367
|
return target.finish();
|
|
325
368
|
};
|
|
326
369
|
const dependencies = new WeakMap<object, readonly string[]>();
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
370
|
+
// Distinct dependency subsets are few (one per rule expression); each gets a
|
|
371
|
+
// small index, and equal subsets from different expressions share it.
|
|
372
|
+
const subsetIndexByKey = new Map<string, number>();
|
|
373
|
+
const subsetIndexByIds = new Map<readonly string[], number>();
|
|
374
|
+
// Invocation-scoped like the pool: cached lists live in flat arrays indexed
|
|
375
|
+
// by subset and cell position, released with this reader and never attached
|
|
376
|
+
// to a prepared cell.
|
|
377
|
+
const listsBySubset: ((readonly DiagnosticSourceLocation[] | undefined)[])[] = [];
|
|
378
|
+
const subsetIndex = (ids: readonly string[]): number => {
|
|
379
|
+
let index = subsetIndexByIds.get(ids);
|
|
380
|
+
if (index === undefined) {
|
|
381
|
+
const key = canonicalJson(ids);
|
|
382
|
+
index = subsetIndexByKey.get(key);
|
|
383
|
+
if (index === undefined) {
|
|
384
|
+
index = listsBySubset.length;
|
|
385
|
+
listsBySubset.push(new Array(cellCount));
|
|
386
|
+
subsetIndexByKey.set(key, index);
|
|
387
|
+
}
|
|
388
|
+
subsetIndexByIds.set(ids, index);
|
|
389
|
+
}
|
|
390
|
+
return index;
|
|
391
|
+
};
|
|
392
|
+
// The one citation list most of a cell's subsets resolve to, and its owned list.
|
|
393
|
+
const cellSingletons: (
|
|
394
|
+
| { readonly cited: readonly DiagnosticSourceLocation[]; readonly list: readonly DiagnosticSourceLocation[] }
|
|
395
|
+
| undefined
|
|
396
|
+
)[] = new Array(cellCount);
|
|
332
397
|
const measureIds = (
|
|
333
398
|
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
334
399
|
): readonly string[] => {
|
|
@@ -341,24 +406,46 @@ function createCompactCellSourceReader() {
|
|
|
341
406
|
};
|
|
342
407
|
const forCell = (
|
|
343
408
|
cell: PreparedDiagnosticData["cells"][number],
|
|
409
|
+
cellIndex: number,
|
|
344
410
|
ids: readonly string[],
|
|
345
411
|
): readonly DiagnosticSourceLocation[] => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
key = canonicalJson(ids);
|
|
349
|
-
dependencyKeys.set(ids, key);
|
|
350
|
-
}
|
|
351
|
-
let subsets = byCell.get(cell);
|
|
352
|
-
if (!subsets) {
|
|
353
|
-
subsets = new Map();
|
|
354
|
-
byCell.set(cell, subsets);
|
|
355
|
-
}
|
|
356
|
-
let sources = subsets.get(key);
|
|
412
|
+
const lists = listsBySubset[subsetIndex(ids)]!;
|
|
413
|
+
let sources = lists[cellIndex];
|
|
357
414
|
if (!sources) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
415
|
+
// Most subsets cite exactly one citation list of one prepared source
|
|
416
|
+
// (the cell's single row) and no blockers: resolve that list directly.
|
|
417
|
+
// Any other shape goes through the general collection.
|
|
418
|
+
let only: readonly DiagnosticSourceLocation[] | null | undefined;
|
|
419
|
+
for (let position = 0; position < ids.length && only !== null; position++) {
|
|
420
|
+
const id = ids[position]!;
|
|
421
|
+
const contributions = cell.contributions[id];
|
|
422
|
+
if (contributions !== undefined)
|
|
423
|
+
for (let index = 0; index < contributions.length; index++) {
|
|
424
|
+
const cited = contributions[index]!.sources;
|
|
425
|
+
if (only === undefined) only = cited;
|
|
426
|
+
else if (only !== cited) {
|
|
427
|
+
only = null;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const blockers = cell.structuralBlockers[id];
|
|
432
|
+
if (blockers !== undefined && blockers.length > 0) only = null;
|
|
433
|
+
}
|
|
434
|
+
if (only === undefined) sources = pool.emptyList;
|
|
435
|
+
else if (only !== null && only.length === 1) {
|
|
436
|
+
const cached = cellSingletons[cellIndex];
|
|
437
|
+
if (cached !== undefined && cached.cited === only) sources = cached.list;
|
|
438
|
+
else {
|
|
439
|
+
sources = pool.singletonForPrepared(only[0]!);
|
|
440
|
+
cellSingletons[cellIndex] = { cited: only, list: sources };
|
|
441
|
+
}
|
|
442
|
+
} else if (only !== null && only.length === 0) sources = pool.emptyList;
|
|
443
|
+
else {
|
|
444
|
+
const target = pool.collection();
|
|
445
|
+
addCellSources(target, cell, ids);
|
|
446
|
+
sources = target.finish();
|
|
447
|
+
}
|
|
448
|
+
lists[cellIndex] = sources;
|
|
362
449
|
}
|
|
363
450
|
return sources;
|
|
364
451
|
};
|
|
@@ -377,45 +464,174 @@ function createCellExpressionEvaluator(
|
|
|
377
464
|
// Reuse projections across this review's rules, then release them before the
|
|
378
465
|
// receipt identity is constructed. Retaining a prepared cell must not retain
|
|
379
466
|
// this temporary bookkeeping or reuse another review's state.
|
|
380
|
-
const statesByCell
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
467
|
+
const statesByCell: (Record<string, FinalizedDiagnosticMeasure> | undefined)[] =
|
|
468
|
+
new Array(prepared.cells.length);
|
|
469
|
+
const stateCache: StateCache = new Map();
|
|
470
|
+
// Compact mode: the two operands of an evaluation are read and dropped, so
|
|
471
|
+
// one mutable operand per side is reused. The eager path keeps fresh objects.
|
|
472
|
+
const reusable: MutableOperand[] | undefined = compactSources
|
|
473
|
+
? [
|
|
474
|
+
{ value: null, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES },
|
|
475
|
+
{ value: null, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES },
|
|
476
|
+
]
|
|
477
|
+
: undefined;
|
|
478
|
+
const operand = (
|
|
479
|
+
slot: number,
|
|
480
|
+
value: number | null,
|
|
481
|
+
reasons: readonly DiagnosticRuleNotEvaluatedReason[],
|
|
482
|
+
overflows: readonly DiagnosticReviewExpressionOverflow[],
|
|
483
|
+
sources: readonly DiagnosticSourceLocation[],
|
|
484
|
+
): EvaluatedOperand => {
|
|
485
|
+
if (reusable === undefined || slot < 0) return { value, reasons, overflows, sources };
|
|
486
|
+
const target = reusable[slot]!;
|
|
487
|
+
target.value = value;
|
|
488
|
+
target.reasons = reasons;
|
|
489
|
+
target.overflows = overflows;
|
|
490
|
+
target.sources = sources;
|
|
491
|
+
return target;
|
|
492
|
+
};
|
|
384
493
|
return (
|
|
385
494
|
cell: PreparedDiagnosticData["cells"][number],
|
|
495
|
+
cellIndex: number,
|
|
386
496
|
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
387
497
|
path: string,
|
|
498
|
+
slot = -1,
|
|
388
499
|
): EvaluatedOperand => {
|
|
389
|
-
let measures = statesByCell
|
|
500
|
+
let measures = statesByCell[cellIndex];
|
|
390
501
|
if (!measures) {
|
|
391
|
-
measures = states(prepared, cell);
|
|
392
|
-
statesByCell
|
|
502
|
+
measures = states(prepared, cell, stateCache);
|
|
503
|
+
statesByCell[cellIndex] = measures;
|
|
504
|
+
}
|
|
505
|
+
const sources = compactSources
|
|
506
|
+
? compactSources.forCell(cell, cellIndex, compactSources.measureIds(expression))
|
|
507
|
+
: cellSources(cell, expressionMeasureIds(expression));
|
|
508
|
+
// A measure leaf is read straight from the projected state: the generic
|
|
509
|
+
// evaluator would build the same result and hand it back unchanged.
|
|
510
|
+
if (expression.op === "measure") {
|
|
511
|
+
const state = hasDiagnosticOwn(measures, expression.measureId)
|
|
512
|
+
? measures[expression.measureId]
|
|
513
|
+
: undefined;
|
|
514
|
+
if (state === undefined)
|
|
515
|
+
return operand(slot, null, ["missing"], NO_REVIEW_OVERFLOWS, sources);
|
|
516
|
+
const overflows = state.expressionOverflows;
|
|
517
|
+
return operand(
|
|
518
|
+
slot,
|
|
519
|
+
state.quantity.value,
|
|
520
|
+
state.readiness,
|
|
521
|
+
overflows === undefined || overflows.length === 0
|
|
522
|
+
? NO_REVIEW_OVERFLOWS
|
|
523
|
+
: overflows.map((overflow) => ({ ...overflow, coordinate: coordinate(cell), sources })),
|
|
524
|
+
sources,
|
|
525
|
+
);
|
|
393
526
|
}
|
|
394
527
|
const result = evaluateDiagnosticMeasureExpression(
|
|
395
528
|
expression,
|
|
396
529
|
measures,
|
|
397
530
|
path,
|
|
398
531
|
);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
532
|
+
return operand(
|
|
533
|
+
slot,
|
|
534
|
+
result.value,
|
|
535
|
+
result.reasons,
|
|
536
|
+
result.overflows.length === 0
|
|
537
|
+
? NO_REVIEW_OVERFLOWS
|
|
538
|
+
: result.overflows.map((overflow) => ({
|
|
539
|
+
...overflow,
|
|
540
|
+
coordinate: coordinate(cell),
|
|
541
|
+
sources,
|
|
542
|
+
})),
|
|
410
543
|
sources,
|
|
411
|
-
|
|
544
|
+
);
|
|
412
545
|
};
|
|
413
546
|
}
|
|
414
547
|
|
|
548
|
+
interface MutableOperand {
|
|
549
|
+
value: number | null;
|
|
550
|
+
reasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
551
|
+
overflows: readonly DiagnosticReviewExpressionOverflow[];
|
|
552
|
+
sources: readonly DiagnosticSourceLocation[];
|
|
553
|
+
}
|
|
554
|
+
|
|
415
555
|
function constant(value: number): EvaluatedOperand {
|
|
416
556
|
return { value, reasons: [], overflows: [], sources: [] };
|
|
417
557
|
}
|
|
418
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Compact-mode reuse. The compact builder copies every field it keeps from an
|
|
561
|
+
* evaluation record and its scope, so one mutable record, one cell scope and
|
|
562
|
+
* one valuation-pair scope per invocation can carry every evaluation to it.
|
|
563
|
+
* The eager path retains its records and never receives these.
|
|
564
|
+
*/
|
|
565
|
+
interface EvaluationScratch {
|
|
566
|
+
readonly record: {
|
|
567
|
+
ruleId: string;
|
|
568
|
+
ruleKind: DiagnosticReviewRule["kind"];
|
|
569
|
+
severity: "warning" | "fail";
|
|
570
|
+
scope: DiagnosticReviewEvaluationScope;
|
|
571
|
+
status: DiagnosticReviewRuleEvaluationBase["status"];
|
|
572
|
+
triggerReason: DiagnosticReviewRuleEvaluationBase["triggerReason"];
|
|
573
|
+
left: number | null;
|
|
574
|
+
right: number | null;
|
|
575
|
+
relation: DiagnosticReviewRuleEvaluationBase["relation"];
|
|
576
|
+
notEvaluatedReasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
577
|
+
expressionOverflows: readonly DiagnosticReviewExpressionOverflow[];
|
|
578
|
+
comparability: unknown;
|
|
579
|
+
};
|
|
580
|
+
readonly cellScope: {
|
|
581
|
+
readonly kind: "cell";
|
|
582
|
+
cell: DiagnosticReviewCoordinate;
|
|
583
|
+
sources: readonly DiagnosticSourceLocation[];
|
|
584
|
+
};
|
|
585
|
+
readonly pairScope: {
|
|
586
|
+
readonly kind: "valuation-pair";
|
|
587
|
+
previous: DiagnosticReviewCoordinate;
|
|
588
|
+
current: DiagnosticReviewCoordinate;
|
|
589
|
+
sources: readonly DiagnosticSourceLocation[];
|
|
590
|
+
};
|
|
591
|
+
/** Constant operands by value; they are read, never retained. */
|
|
592
|
+
readonly constants: Map<number, EvaluatedOperand>;
|
|
593
|
+
}
|
|
594
|
+
function createEvaluationScratch(): EvaluationScratch {
|
|
595
|
+
const placeholder: DiagnosticReviewCoordinate = {
|
|
596
|
+
sourceGroup: "",
|
|
597
|
+
origin: "",
|
|
598
|
+
valuation: "",
|
|
599
|
+
developmentAge: 0,
|
|
600
|
+
ageUnit: "",
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
record: {
|
|
604
|
+
ruleId: "",
|
|
605
|
+
ruleKind: "compare",
|
|
606
|
+
severity: "warning",
|
|
607
|
+
scope: { kind: "cell", cell: placeholder, sources: NO_SOURCES },
|
|
608
|
+
status: "pass",
|
|
609
|
+
triggerReason: null,
|
|
610
|
+
left: null,
|
|
611
|
+
right: null,
|
|
612
|
+
relation: null,
|
|
613
|
+
notEvaluatedReasons: NO_REASONS,
|
|
614
|
+
expressionOverflows: NO_REVIEW_OVERFLOWS,
|
|
615
|
+
comparability: undefined,
|
|
616
|
+
},
|
|
617
|
+
cellScope: { kind: "cell", cell: placeholder, sources: NO_SOURCES },
|
|
618
|
+
pairScope: { kind: "valuation-pair", previous: placeholder, current: placeholder, sources: NO_SOURCES },
|
|
619
|
+
constants: new Map(),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Empty lists owned by one review invocation; its evaluations may share them. */
|
|
624
|
+
interface OwnedEmptyLists {
|
|
625
|
+
readonly reasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
626
|
+
readonly overflows: readonly DiagnosticReviewExpressionOverflow[];
|
|
627
|
+
}
|
|
628
|
+
// One empty array serves both roles, exactly as the eager finalizer's interner
|
|
629
|
+
// would merge equal frozen empties into a single shared instance.
|
|
630
|
+
const freshEmptyLists = (): OwnedEmptyLists => {
|
|
631
|
+
const empty = Object.freeze([]) as readonly never[];
|
|
632
|
+
return { reasons: empty, overflows: empty };
|
|
633
|
+
};
|
|
634
|
+
|
|
419
635
|
function evaluation(
|
|
420
636
|
prepared: PreparedDiagnosticDataContent,
|
|
421
637
|
rule: DiagnosticReviewRule,
|
|
@@ -423,9 +639,11 @@ function evaluation(
|
|
|
423
639
|
left: EvaluatedOperand,
|
|
424
640
|
right: EvaluatedOperand,
|
|
425
641
|
passes: (relation: "less" | "equal" | "greater") => boolean,
|
|
426
|
-
extraReasons: readonly DiagnosticRuleNotEvaluatedReason[] =
|
|
642
|
+
extraReasons: readonly DiagnosticRuleNotEvaluatedReason[] = NO_REASONS,
|
|
643
|
+
empty: OwnedEmptyLists = freshEmptyLists(),
|
|
644
|
+
scratch?: EvaluationScratch,
|
|
427
645
|
): DiagnosticReviewRuleEvaluation {
|
|
428
|
-
const expressionOverflows = [
|
|
646
|
+
const expressionOverflows = left.overflows.length === 0 && right.overflows.length === 0 ? [] : [
|
|
429
647
|
...new Map(
|
|
430
648
|
[...left.overflows, ...right.overflows].map((item) => [
|
|
431
649
|
canonicalJson(item),
|
|
@@ -475,88 +693,144 @@ function evaluation(
|
|
|
475
693
|
}
|
|
476
694
|
return a.sources.length - b.sources.length;
|
|
477
695
|
});
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
...right.reasons,
|
|
481
|
-
...extraReasons,
|
|
482
|
-
...(expressionOverflows.length > 0 ? ["expression-overflow" as const] : []),
|
|
483
|
-
...((left.value === null || right.value === null) &&
|
|
696
|
+
// Clean evaluated operands carry no reasons; skip the spread/filter round trip.
|
|
697
|
+
const reasons: readonly DiagnosticRuleNotEvaluatedReason[] =
|
|
484
698
|
left.reasons.length === 0 &&
|
|
485
699
|
right.reasons.length === 0 &&
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
};
|
|
504
|
-
const result = (
|
|
505
|
-
value: Omit<
|
|
506
|
-
DiagnosticReviewRuleEvaluationBase,
|
|
507
|
-
"ruleId" | "ruleKind" | "severity" | "scope"
|
|
508
|
-
>,
|
|
509
|
-
): DiagnosticReviewRuleEvaluation =>
|
|
510
|
-
({
|
|
511
|
-
ruleId: rule.id,
|
|
512
|
-
ruleKind: rule.kind,
|
|
513
|
-
severity: rule.severity,
|
|
514
|
-
scope,
|
|
515
|
-
...value,
|
|
516
|
-
...(rule.kind === "layer-order"
|
|
517
|
-
? { comparability: rule.comparability }
|
|
518
|
-
: {}),
|
|
519
|
-
}) as DiagnosticReviewRuleEvaluation;
|
|
700
|
+
extraReasons.length === 0 &&
|
|
701
|
+
expressionOverflows.length === 0 &&
|
|
702
|
+
left.value !== null &&
|
|
703
|
+
right.value !== null
|
|
704
|
+
? []
|
|
705
|
+
: uniqueReasons([
|
|
706
|
+
...left.reasons,
|
|
707
|
+
...right.reasons,
|
|
708
|
+
...extraReasons,
|
|
709
|
+
...(expressionOverflows.length > 0 ? ["expression-overflow" as const] : []),
|
|
710
|
+
...((left.value === null || right.value === null) &&
|
|
711
|
+
left.reasons.length === 0 &&
|
|
712
|
+
right.reasons.length === 0 &&
|
|
713
|
+
expressionOverflows.length === 0
|
|
714
|
+
? ["missing" as const]
|
|
715
|
+
: []),
|
|
716
|
+
]);
|
|
520
717
|
if (reasons.length > 0)
|
|
521
|
-
return
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
left
|
|
525
|
-
right
|
|
526
|
-
|
|
527
|
-
|
|
718
|
+
return evaluationRecord(
|
|
719
|
+
rule,
|
|
720
|
+
scope,
|
|
721
|
+
left,
|
|
722
|
+
right,
|
|
723
|
+
rule.missingInput === "finding" ? "triggered" : "not-evaluated",
|
|
724
|
+
rule.missingInput === "finding" ? missingTriggerReason(reasons) : null,
|
|
725
|
+
null,
|
|
726
|
+
reasons,
|
|
528
727
|
expressionOverflows,
|
|
529
|
-
|
|
728
|
+
scratch,
|
|
729
|
+
);
|
|
530
730
|
const compared = classifyDiagnosticComparison(
|
|
531
731
|
left.value,
|
|
532
732
|
right.value,
|
|
533
733
|
rule.tolerance,
|
|
534
734
|
);
|
|
735
|
+
// Empty reason and overflow lists are this review's own immutable values;
|
|
736
|
+
// the eager finalizer leaves frozen children as they are and readers only
|
|
737
|
+
// iterate them, and no other invocation ever sees the same arrays.
|
|
535
738
|
if (compared.status === "not-evaluated")
|
|
536
|
-
return
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
739
|
+
return evaluationRecord(
|
|
740
|
+
rule,
|
|
741
|
+
scope,
|
|
742
|
+
left,
|
|
743
|
+
right,
|
|
744
|
+
rule.missingInput === "finding" ? "triggered" : "not-evaluated",
|
|
745
|
+
rule.missingInput === "finding"
|
|
746
|
+
? compared.reason === "tolerance-overflow"
|
|
747
|
+
? "tolerance-overflow"
|
|
748
|
+
: "missing-input"
|
|
749
|
+
: null,
|
|
750
|
+
null,
|
|
751
|
+
[compared.reason],
|
|
752
|
+
empty.overflows,
|
|
753
|
+
scratch,
|
|
754
|
+
);
|
|
550
755
|
const pass = passes(compared.relation);
|
|
551
|
-
return
|
|
552
|
-
|
|
553
|
-
|
|
756
|
+
return evaluationRecord(
|
|
757
|
+
rule,
|
|
758
|
+
scope,
|
|
759
|
+
left,
|
|
760
|
+
right,
|
|
761
|
+
pass ? "pass" : "triggered",
|
|
762
|
+
pass ? null : "predicate",
|
|
763
|
+
compared.relation,
|
|
764
|
+
empty.reasons,
|
|
765
|
+
empty.overflows,
|
|
766
|
+
scratch,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function missingTriggerReason(
|
|
771
|
+
reasons: readonly DiagnosticRuleNotEvaluatedReason[],
|
|
772
|
+
): DiagnosticReviewRuleEvaluation["triggerReason"] {
|
|
773
|
+
if (
|
|
774
|
+
reasons.some((reason) =>
|
|
775
|
+
["missing", "imputed", "non-finite", "structural-ambiguity"].includes(
|
|
776
|
+
reason,
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
return "missing-input";
|
|
781
|
+
if (reasons.includes("aggregation-overflow")) return "aggregation-overflow";
|
|
782
|
+
if (reasons.includes("expression-overflow")) return "expression-overflow";
|
|
783
|
+
if (reasons.includes("tolerance-overflow")) return "tolerance-overflow";
|
|
784
|
+
return "missing-input";
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** One literal in the contract's key order; no intermediate spread objects. */
|
|
788
|
+
function evaluationRecord(
|
|
789
|
+
rule: DiagnosticReviewRule,
|
|
790
|
+
scope: DiagnosticReviewEvaluationScope,
|
|
791
|
+
left: EvaluatedOperand,
|
|
792
|
+
right: EvaluatedOperand,
|
|
793
|
+
status: DiagnosticReviewRuleEvaluationBase["status"],
|
|
794
|
+
triggerReason: DiagnosticReviewRuleEvaluationBase["triggerReason"],
|
|
795
|
+
relation: DiagnosticReviewRuleEvaluationBase["relation"],
|
|
796
|
+
notEvaluatedReasons: readonly DiagnosticRuleNotEvaluatedReason[],
|
|
797
|
+
overflows: readonly DiagnosticReviewExpressionOverflow[],
|
|
798
|
+
scratch?: EvaluationScratch,
|
|
799
|
+
): DiagnosticReviewRuleEvaluation {
|
|
800
|
+
if (scratch !== undefined) {
|
|
801
|
+
const target = scratch.record;
|
|
802
|
+
target.ruleId = rule.id;
|
|
803
|
+
target.ruleKind = rule.kind;
|
|
804
|
+
target.severity = rule.severity;
|
|
805
|
+
target.scope = scope;
|
|
806
|
+
target.status = status;
|
|
807
|
+
target.triggerReason = triggerReason;
|
|
808
|
+
target.left = left.value;
|
|
809
|
+
target.right = right.value;
|
|
810
|
+
target.relation = relation;
|
|
811
|
+
target.notEvaluatedReasons = notEvaluatedReasons;
|
|
812
|
+
target.expressionOverflows = overflows;
|
|
813
|
+
target.comparability = rule.kind === "layer-order" ? rule.comparability : undefined;
|
|
814
|
+
return target as unknown as DiagnosticReviewRuleEvaluation;
|
|
815
|
+
}
|
|
816
|
+
const value = {
|
|
817
|
+
ruleId: rule.id,
|
|
818
|
+
ruleKind: rule.kind,
|
|
819
|
+
severity: rule.severity,
|
|
820
|
+
scope,
|
|
821
|
+
status,
|
|
822
|
+
triggerReason,
|
|
554
823
|
left: left.value,
|
|
555
824
|
right: right.value,
|
|
556
|
-
relation
|
|
557
|
-
notEvaluatedReasons
|
|
558
|
-
expressionOverflows:
|
|
559
|
-
}
|
|
825
|
+
relation,
|
|
826
|
+
notEvaluatedReasons,
|
|
827
|
+
expressionOverflows: overflows,
|
|
828
|
+
};
|
|
829
|
+
return (
|
|
830
|
+
rule.kind === "layer-order"
|
|
831
|
+
? { ...value, comparability: rule.comparability }
|
|
832
|
+
: value
|
|
833
|
+
) as DiagnosticReviewRuleEvaluation;
|
|
560
834
|
}
|
|
561
835
|
|
|
562
836
|
function filterCells(
|
|
@@ -720,7 +994,7 @@ export function evaluateDiagnosticReviewRulesCompact(
|
|
|
720
994
|
const invariant = parent
|
|
721
995
|
? compactEvaluationCache.get(parent) ?? evaluateDiagnosticReviewRulesCompact(parent)
|
|
722
996
|
: undefined;
|
|
723
|
-
const compactSources = createCompactCellSourceReader();
|
|
997
|
+
const compactSources = createCompactCellSourceReader(prepared.cells.length);
|
|
724
998
|
const builder = createCompactReviewBuilder(
|
|
725
999
|
prepared.definition.definition.reviewRules,
|
|
726
1000
|
compactSources,
|
|
@@ -747,10 +1021,21 @@ function visitDiagnosticReviewRules(
|
|
|
747
1021
|
prepared,
|
|
748
1022
|
compactSources,
|
|
749
1023
|
);
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1024
|
+
const ownedEmpty = freshEmptyLists();
|
|
1025
|
+
// Only the compact builder, which copies what it keeps, receives reused records.
|
|
1026
|
+
const scratch = compactSources === undefined ? undefined : createEvaluationScratch();
|
|
1027
|
+
const constantOperand = (value: number): EvaluatedOperand => {
|
|
1028
|
+
if (scratch === undefined) return constant(value);
|
|
1029
|
+
let operand = scratch.constants.get(value);
|
|
1030
|
+
if (operand === undefined) {
|
|
1031
|
+
operand = { value, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES };
|
|
1032
|
+
scratch.constants.set(value, operand);
|
|
1033
|
+
}
|
|
1034
|
+
return operand;
|
|
1035
|
+
};
|
|
1036
|
+
const cells = prepared.cells;
|
|
1037
|
+
// Cell positions, not cell objects, key every per-cell cache in this walk.
|
|
1038
|
+
const cellIndexByCoordinate = new Map<string, number>();
|
|
754
1039
|
const expectedSourcesByCoordinate = new Map<
|
|
755
1040
|
string,
|
|
756
1041
|
DiagnosticSourceLocation[]
|
|
@@ -759,8 +1044,29 @@ function visitDiagnosticReviewRules(
|
|
|
759
1044
|
sourceGroup: string,
|
|
760
1045
|
origin: string,
|
|
761
1046
|
valuation: string,
|
|
762
|
-
) =>
|
|
763
|
-
|
|
1047
|
+
) => canonicalScalarArrayJson([sourceGroup, origin, valuation]);
|
|
1048
|
+
// One review coordinate per cell; every rule's scope for that cell cites it.
|
|
1049
|
+
const coordinates: (DiagnosticReviewCoordinate | undefined)[] = new Array(cells.length);
|
|
1050
|
+
const coordinateOf = (
|
|
1051
|
+
cell: PreparedDiagnosticData["cells"][number],
|
|
1052
|
+
cellIndex: number,
|
|
1053
|
+
): DiagnosticReviewCoordinate => {
|
|
1054
|
+
let value = coordinates[cellIndex];
|
|
1055
|
+
if (!value) {
|
|
1056
|
+
value = coordinate(cell);
|
|
1057
|
+
coordinates[cellIndex] = value;
|
|
1058
|
+
}
|
|
1059
|
+
return value;
|
|
1060
|
+
};
|
|
1061
|
+
// Cells and expected-cell sources are resolved once per group; every
|
|
1062
|
+
// monotonic rule then walks the same ordered valuations by position.
|
|
1063
|
+
const monotonicGroups: Array<{
|
|
1064
|
+
sourceGroup: string;
|
|
1065
|
+
origin: string;
|
|
1066
|
+
valuations: string[];
|
|
1067
|
+
cellIndices: (number | undefined)[];
|
|
1068
|
+
expected: (readonly DiagnosticSourceLocation[])[];
|
|
1069
|
+
}> = [];
|
|
764
1070
|
// Keep the original traversal and source order; only replace repeated searches.
|
|
765
1071
|
// Other rule families do not need these indices.
|
|
766
1072
|
if (
|
|
@@ -768,9 +1074,10 @@ function visitDiagnosticReviewRules(
|
|
|
768
1074
|
(rule) => rule.kind === "monotonic",
|
|
769
1075
|
)
|
|
770
1076
|
) {
|
|
771
|
-
for (
|
|
1077
|
+
for (let index = 0; index < cells.length; index++) {
|
|
1078
|
+
const cell = cells[index]!;
|
|
772
1079
|
const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
|
|
773
|
-
if (!
|
|
1080
|
+
if (!cellIndexByCoordinate.has(key)) cellIndexByCoordinate.set(key, index);
|
|
774
1081
|
}
|
|
775
1082
|
for (const cell of prepared.expectedCells) {
|
|
776
1083
|
if (!cell.source) continue;
|
|
@@ -783,7 +1090,7 @@ function visitDiagnosticReviewRules(
|
|
|
783
1090
|
// the rule expression. Build them once for all monotonic rules in this run.
|
|
784
1091
|
const groups = new Map<string, Set<string>>();
|
|
785
1092
|
for (const cell of [...prepared.cells, ...prepared.expectedCells]) {
|
|
786
|
-
const key =
|
|
1093
|
+
const key = canonicalScalarArrayJson([cell.sourceGroup, cell.origin]);
|
|
787
1094
|
const values = groups.get(key) ?? new Set<string>();
|
|
788
1095
|
values.add(cell.valuation);
|
|
789
1096
|
groups.set(key, values);
|
|
@@ -801,7 +1108,15 @@ function visitDiagnosticReviewRules(
|
|
|
801
1108
|
const [sourceGroup, origin] = JSON.parse(key) as [string, string];
|
|
802
1109
|
const valuations = [...values].sort((left, right) =>
|
|
803
1110
|
valuationCoordinate(left) - valuationCoordinate(right) || codeUnit(left, right));
|
|
804
|
-
monotonicGroups.push({
|
|
1111
|
+
monotonicGroups.push({
|
|
1112
|
+
sourceGroup,
|
|
1113
|
+
origin,
|
|
1114
|
+
valuations,
|
|
1115
|
+
cellIndices: valuations.map((valuation) =>
|
|
1116
|
+
cellIndexByCoordinate.get(coordinateKey(sourceGroup, origin, valuation))),
|
|
1117
|
+
expected: valuations.map((valuation) =>
|
|
1118
|
+
expectedSourcesByCoordinate.get(coordinateKey(sourceGroup, origin, valuation)) ?? NO_SOURCES),
|
|
1119
|
+
});
|
|
805
1120
|
}
|
|
806
1121
|
}
|
|
807
1122
|
for (const [ruleIndex, rule] of (
|
|
@@ -947,84 +1262,77 @@ function visitDiagnosticReviewRules(
|
|
|
947
1262
|
left,
|
|
948
1263
|
constant(rule.expected),
|
|
949
1264
|
(relation) => relation === "equal",
|
|
1265
|
+
NO_REASONS,
|
|
1266
|
+
ownedEmpty,
|
|
950
1267
|
),
|
|
951
1268
|
);
|
|
952
1269
|
continue;
|
|
953
1270
|
}
|
|
954
1271
|
if (rule.kind === "monotonic") {
|
|
955
|
-
|
|
1272
|
+
const expressionPath = `${basePath}/expression`;
|
|
1273
|
+
const monotonicPasses = (relation: "less" | "equal" | "greater") =>
|
|
1274
|
+
rule.direction === "nondecreasing" ? relation !== "greater" : relation !== "less";
|
|
1275
|
+
for (const { sourceGroup, origin, valuations: sorted, cellIndices, expected } of monotonicGroups) {
|
|
956
1276
|
for (let index = 1; index < sorted.length; index++) {
|
|
957
1277
|
const previousValuation = sorted[index - 1]!;
|
|
958
1278
|
const currentValuation = sorted[index]!;
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const current =
|
|
963
|
-
coordinateKey(sourceGroup, origin, currentValuation),
|
|
964
|
-
);
|
|
965
|
-
const expectedSources = (valuation: string) =>
|
|
966
|
-
expectedSourcesByCoordinate.get(
|
|
967
|
-
coordinateKey(sourceGroup, origin, valuation),
|
|
968
|
-
) ?? [];
|
|
1279
|
+
const previousIndex = cellIndices[index - 1];
|
|
1280
|
+
const currentIndex = cellIndices[index];
|
|
1281
|
+
const previous = previousIndex === undefined ? undefined : cells[previousIndex];
|
|
1282
|
+
const current = currentIndex === undefined ? undefined : cells[currentIndex];
|
|
969
1283
|
const left = previous
|
|
970
|
-
? evaluateExpression(
|
|
971
|
-
previous,
|
|
972
|
-
rule.expression,
|
|
973
|
-
`${basePath}/expression`,
|
|
974
|
-
)
|
|
1284
|
+
? evaluateExpression(previous, previousIndex!, rule.expression, expressionPath, 0)
|
|
975
1285
|
: {
|
|
976
1286
|
value: null,
|
|
977
1287
|
reasons: ["missing" as const],
|
|
978
1288
|
overflows: [],
|
|
979
1289
|
sources: compactSources
|
|
980
|
-
? compactSources.forPreparedSources(
|
|
981
|
-
|
|
982
|
-
)
|
|
983
|
-
: uniqueSources(expectedSources(previousValuation)),
|
|
1290
|
+
? compactSources.forPreparedSources(expected[index - 1]!)
|
|
1291
|
+
: uniqueSources(expected[index - 1]!),
|
|
984
1292
|
};
|
|
985
1293
|
const right = current
|
|
986
|
-
? evaluateExpression(
|
|
987
|
-
current,
|
|
988
|
-
rule.expression,
|
|
989
|
-
`${basePath}/expression`,
|
|
990
|
-
)
|
|
1294
|
+
? evaluateExpression(current, currentIndex!, rule.expression, expressionPath, 1)
|
|
991
1295
|
: {
|
|
992
1296
|
value: null,
|
|
993
1297
|
reasons: ["missing" as const],
|
|
994
1298
|
overflows: [],
|
|
995
1299
|
sources: compactSources
|
|
996
|
-
? compactSources.forPreparedSources(
|
|
997
|
-
|
|
998
|
-
)
|
|
999
|
-
: uniqueSources(expectedSources(currentValuation)),
|
|
1300
|
+
? compactSources.forPreparedSources(expected[index]!)
|
|
1301
|
+
: uniqueSources(expected[index]!),
|
|
1000
1302
|
};
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
:
|
|
1022
|
-
|
|
1303
|
+
const previousCoordinate = previous
|
|
1304
|
+
? coordinateOf(previous, previousIndex!)
|
|
1305
|
+
: coordinateFromLabels(prepared, sourceGroup, origin, previousValuation);
|
|
1306
|
+
const currentCoordinate = current
|
|
1307
|
+
? coordinateOf(current, currentIndex!)
|
|
1308
|
+
: coordinateFromLabels(prepared, sourceGroup, origin, currentValuation);
|
|
1309
|
+
const pairSources = compactSources
|
|
1310
|
+
? compactSources.union([left.sources, right.sources])
|
|
1311
|
+
: uniqueSources([...left.sources, ...right.sources]);
|
|
1312
|
+
let scope: DiagnosticReviewEvaluationScope;
|
|
1313
|
+
if (scratch !== undefined) {
|
|
1314
|
+
scratch.pairScope.previous = previousCoordinate;
|
|
1315
|
+
scratch.pairScope.current = currentCoordinate;
|
|
1316
|
+
scratch.pairScope.sources = pairSources;
|
|
1317
|
+
scope = scratch.pairScope;
|
|
1318
|
+
} else
|
|
1319
|
+
scope = {
|
|
1320
|
+
kind: "valuation-pair",
|
|
1321
|
+
previous: previousCoordinate,
|
|
1322
|
+
current: currentCoordinate,
|
|
1323
|
+
sources: pairSources,
|
|
1324
|
+
};
|
|
1023
1325
|
appendEvaluation(
|
|
1024
|
-
evaluation(
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1326
|
+
evaluation(
|
|
1327
|
+
prepared,
|
|
1328
|
+
rule,
|
|
1329
|
+
scope,
|
|
1330
|
+
left,
|
|
1331
|
+
right,
|
|
1332
|
+
monotonicPasses,
|
|
1333
|
+
NO_REASONS,
|
|
1334
|
+
ownedEmpty,
|
|
1335
|
+
scratch,
|
|
1028
1336
|
),
|
|
1029
1337
|
);
|
|
1030
1338
|
}
|
|
@@ -1051,75 +1359,65 @@ function visitDiagnosticReviewRules(
|
|
|
1051
1359
|
].sort(codeUnit),
|
|
1052
1360
|
)
|
|
1053
1361
|
: undefined;
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1362
|
+
// Operand paths depend only on the rule; build them once, not per cell.
|
|
1363
|
+
const leftPath = `${basePath}/when/left`;
|
|
1364
|
+
const rightPath = `${basePath}/when/right`;
|
|
1365
|
+
const actualPath = `${basePath}/actual`;
|
|
1366
|
+
const expectedPath = `${basePath}/expected`;
|
|
1367
|
+
const narrowerPath = `${basePath}/narrower`;
|
|
1368
|
+
const broaderPath = `${basePath}/broader`;
|
|
1369
|
+
// Per-rule helpers: created once, not once per cell.
|
|
1370
|
+
const scopeFor = (
|
|
1371
|
+
cell: PreparedDiagnosticData["cells"][number],
|
|
1372
|
+
cellIndex: number,
|
|
1373
|
+
leftSources: readonly DiagnosticSourceLocation[],
|
|
1374
|
+
rightSources: readonly DiagnosticSourceLocation[],
|
|
1375
|
+
): DiagnosticCellReviewScope => {
|
|
1376
|
+
if (scratch !== undefined) {
|
|
1377
|
+
scratch.cellScope.cell = coordinateOf(cell, cellIndex);
|
|
1378
|
+
scratch.cellScope.sources = compactSources!.forCell(cell, cellIndex, scopeDependencies!);
|
|
1379
|
+
return scratch.cellScope;
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1059
1382
|
kind: "cell",
|
|
1060
|
-
cell:
|
|
1383
|
+
cell: coordinateOf(cell, cellIndex),
|
|
1061
1384
|
sources: compactSources
|
|
1062
|
-
? compactSources.forCell(cell, scopeDependencies!)
|
|
1385
|
+
? compactSources.forCell(cell, cellIndex, scopeDependencies!)
|
|
1063
1386
|
: uniqueSources([...leftSources, ...rightSources]),
|
|
1064
|
-
}
|
|
1387
|
+
};
|
|
1388
|
+
};
|
|
1389
|
+
const comparePasses = (relation: "less" | "equal" | "greater") =>
|
|
1390
|
+
rule.kind === "compare" && !diagnosticPredicateMatches(rule.when.operator, relation);
|
|
1391
|
+
const reconcilePasses = (relation: "less" | "equal" | "greater") => relation === "equal";
|
|
1392
|
+
const layerPasses = (relation: "less" | "equal" | "greater") => relation !== "greater";
|
|
1393
|
+
for (let index = 0; index < cells.length; index++) {
|
|
1394
|
+
const cell = cells[index]!;
|
|
1065
1395
|
if (rule.kind === "compare") {
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
?
|
|
1069
|
-
: evaluateExpression(cell,
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1396
|
+
const left =
|
|
1397
|
+
rule.when.left.op === "constant"
|
|
1398
|
+
? constantOperand(rule.when.left.value)
|
|
1399
|
+
: evaluateExpression(cell, index, rule.when.left, leftPath, 0);
|
|
1400
|
+
const right =
|
|
1401
|
+
rule.when.right.op === "constant"
|
|
1402
|
+
? constantOperand(rule.when.right.value)
|
|
1403
|
+
: evaluateExpression(cell, index, rule.when.right, rightPath, 1);
|
|
1072
1404
|
appendEvaluation(
|
|
1073
|
-
evaluation(
|
|
1074
|
-
prepared,
|
|
1075
|
-
rule,
|
|
1076
|
-
scopeFor(left.sources, right.sources),
|
|
1077
|
-
left,
|
|
1078
|
-
right,
|
|
1079
|
-
(relation) =>
|
|
1080
|
-
!diagnosticPredicateMatches(rule.when.operator, relation),
|
|
1081
|
-
),
|
|
1405
|
+
evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, comparePasses, NO_REASONS, ownedEmpty, scratch),
|
|
1082
1406
|
);
|
|
1083
1407
|
} else if (rule.kind === "reconcile") {
|
|
1084
|
-
const left = evaluateExpression(
|
|
1085
|
-
cell,
|
|
1086
|
-
rule.actual,
|
|
1087
|
-
`${basePath}/actual`,
|
|
1088
|
-
);
|
|
1408
|
+
const left = evaluateExpression(cell, index, rule.actual, actualPath, 0);
|
|
1089
1409
|
const right =
|
|
1090
1410
|
rule.expected.op === "constant"
|
|
1091
|
-
?
|
|
1092
|
-
: evaluateExpression(cell, rule.expected,
|
|
1411
|
+
? constantOperand(rule.expected.value)
|
|
1412
|
+
: evaluateExpression(cell, index, rule.expected, expectedPath, 1);
|
|
1093
1413
|
appendEvaluation(
|
|
1094
|
-
evaluation(
|
|
1095
|
-
prepared,
|
|
1096
|
-
rule,
|
|
1097
|
-
scopeFor(left.sources, right.sources),
|
|
1098
|
-
left,
|
|
1099
|
-
right,
|
|
1100
|
-
(relation) => relation === "equal",
|
|
1101
|
-
),
|
|
1414
|
+
evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, reconcilePasses, NO_REASONS, ownedEmpty, scratch),
|
|
1102
1415
|
);
|
|
1103
1416
|
} else {
|
|
1104
|
-
const left = evaluateExpression(
|
|
1105
|
-
|
|
1106
|
-
rule.narrower,
|
|
1107
|
-
`${basePath}/narrower`,
|
|
1108
|
-
);
|
|
1109
|
-
const right = evaluateExpression(
|
|
1110
|
-
cell,
|
|
1111
|
-
rule.broader,
|
|
1112
|
-
`${basePath}/broader`,
|
|
1113
|
-
);
|
|
1417
|
+
const left = evaluateExpression(cell, index, rule.narrower, narrowerPath, 0);
|
|
1418
|
+
const right = evaluateExpression(cell, index, rule.broader, broaderPath, 1);
|
|
1114
1419
|
appendEvaluation(
|
|
1115
|
-
evaluation(
|
|
1116
|
-
prepared,
|
|
1117
|
-
rule,
|
|
1118
|
-
scopeFor(left.sources, right.sources),
|
|
1119
|
-
left,
|
|
1120
|
-
right,
|
|
1121
|
-
(relation) => relation !== "greater",
|
|
1122
|
-
),
|
|
1420
|
+
evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, layerPasses, NO_REASONS, ownedEmpty, scratch),
|
|
1123
1421
|
);
|
|
1124
1422
|
}
|
|
1125
1423
|
}
|