@portll/cobolwork 0.2.140 → 0.3.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 +14 -2
- package/THIRD-PARTY-NOTICES.md +3 -2
- package/bin/cobolwork.mjs +16 -1
- package/lib/baseline.mjs +8 -6
- package/lib/bms.mjs +1 -1
- package/lib/capabilities.mjs +2 -1
- package/lib/cards.mjs +66 -0
- package/lib/compliance.mjs +6 -5
- package/lib/control.mjs +402 -19
- package/lib/dataflow.mjs +33 -9
- package/lib/diff.mjs +65 -89
- package/lib/equivalence.mjs +30 -12
- package/lib/evidence/record.mjs +3 -1
- package/lib/evidence/slsa.mjs +17 -2
- package/lib/execution.mjs +77 -0
- package/lib/explain.mjs +1 -1
- package/lib/ftp.mjs +1 -1
- package/lib/inventory.mjs +8 -9
- package/lib/jcl.mjs +3 -48
- package/lib/kernel/git.mjs +60 -0
- package/lib/kernel/identity.mjs +7 -6
- package/lib/kernel/source-tree.mjs +204 -34
- package/lib/parser.mjs +31 -15
- package/lib/revision.json +1 -1
- package/lib/sbom.mjs +33 -9
- package/lib/scan.mjs +11 -4
- package/lib/sets/build.mjs +1 -1
- package/lib/sets/cics.mjs +27 -3
- package/lib/sets/copybook.mjs +1 -1
- package/lib/sets/hidden.mjs +1 -1
- package/lib/sets/jcl.mjs +1 -1
- package/lib/sets/priv.mjs +1 -1
- package/lib/sets/recon.mjs +2 -2
- package/lib/sets/semantics.mjs +1 -1
- package/lib/sets/vendor.mjs +1 -1
- package/lib/site.mjs +17 -7
- package/lib/sources.mjs +82 -38
- package/lib/utilities.mjs +1 -1
- package/package.json +2 -2
- package/rules/compliance-cobit2019.json +2438 -0
- package/lib/card.mjs +0 -18
package/lib/control.mjs
CHANGED
|
@@ -251,6 +251,12 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
251
251
|
const loopBody = new Map();
|
|
252
252
|
const stmtAt = new Map();
|
|
253
253
|
for (const st of prog.statements) if (st.at != null && st.verb !== 'WHEN') stmtAt.set(st.at, st);
|
|
254
|
+
const unpassedBy = new Map();
|
|
255
|
+
for (const c of prog.calls || []) {
|
|
256
|
+
const st = prog.statements[c.stmtIndex];
|
|
257
|
+
if (st) unpassedBy.set(st, new Set([c.targetTok, ...c.using.filter((a) => a.mode !== 'REFERENCE' && a.tok).map((a) => a.tok)]));
|
|
258
|
+
}
|
|
259
|
+
const external = (it) => { for (let a = it; a; a = a.parent) if (a.external) return true; return false; };
|
|
254
260
|
const labelAt = new Map();
|
|
255
261
|
for (const l of prog.labels) if (l.at != null) labelAt.set(l.at, l);
|
|
256
262
|
const labelNames = new Set(prog.labels.map((l) => l.name));
|
|
@@ -640,11 +646,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
640
646
|
for (const t of n.st.targets || []) add(t);
|
|
641
647
|
if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
|
|
642
648
|
// A callee may write what it is passed by reference, and a READ fills the file's records.
|
|
649
|
+
// The field naming the program called, and what goes BY CONTENT or BY VALUE, is not passed
|
|
650
|
+
// for the callee to write - unless it is EXTERNAL, which any program can.
|
|
651
|
+
const unpassed = n.st.verb === 'CALL' ? unpassedBy.get(n.st) : null;
|
|
643
652
|
for (const t of n.st.sources || []) {
|
|
644
653
|
const f = resolve(t);
|
|
645
654
|
if (!f) continue;
|
|
646
655
|
if (f.records) for (const r of f.records) out.push(r);
|
|
647
|
-
else if (n.st.verb === 'CALL') out.push(f);
|
|
656
|
+
else if (n.st.verb === 'CALL' && !(unpassed && unpassed.has(t) && !external(f))) out.push(f);
|
|
648
657
|
}
|
|
649
658
|
}
|
|
650
659
|
}
|
|
@@ -1083,7 +1092,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1083
1092
|
const bounds = { lo: 1, hi };
|
|
1084
1093
|
const c = { id: checks.length, key, field, file: list[0].n.st.file, line: list[0].n.st.line, tCons: { numeric: true, lo: 1, loInc: true, hi, hiInc: true }, fCons: null, restricts: false, assigned: true, bounds };
|
|
1085
1094
|
checks.push(c);
|
|
1086
|
-
|
|
1095
|
+
// "At most v" alone, which a decrement that cannot go below 0 leaves standing.
|
|
1096
|
+
const top = { id: checks.length, key, field, file: c.file, line: c.line, tCons: { numeric: true, hi, hiInc: true }, fCons: null, restricts: false, assigned: true, bounds: { hi } };
|
|
1097
|
+
checks.push(top);
|
|
1098
|
+
for (const a of list) if (Number(a.value) <= hi) (a.upTo ||= []).push(c, top);
|
|
1087
1099
|
}
|
|
1088
1100
|
}
|
|
1089
1101
|
// Fact numbers: for each check, `x` that it ran (only a restricting one), `t` and `f` that it came
|
|
@@ -1419,6 +1431,40 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1419
1431
|
(a.n.genStmt ||= []).push(newLo(a.field, v, a.n.st).t);
|
|
1420
1432
|
}
|
|
1421
1433
|
// An increment by at least m turns "at least k" into "at least k + m", where the sum fits the field.
|
|
1434
|
+
// The fields a statement only ever lowers, by a constant, and by how much at most.
|
|
1435
|
+
const decrements = (st) => {
|
|
1436
|
+
const seg = segOf(st);
|
|
1437
|
+
let fields = [];
|
|
1438
|
+
let max = 0;
|
|
1439
|
+
const amount = (t) => { const v = t && (t.t === 'word' || t.t === 'num') ? numberOf(constValue([t])) : null; return v != null && Number.isInteger(v) && v >= 0 ? v : null; };
|
|
1440
|
+
if (st.verb === 'SUBTRACT') {
|
|
1441
|
+
const from = seg.findIndex((t) => WORD(t, 'FROM'));
|
|
1442
|
+
if (from < 1 || seg.some((t) => WORD(t, 'GIVING') || WORD(t, 'CORRESPONDING') || WORD(t, 'CORR'))) return null;
|
|
1443
|
+
for (const t of seg.slice(0, from)) { const v = amount(t); if (v == null) return null; max += v; }
|
|
1444
|
+
const end = seg.findIndex((t, i) => i > from && PHRASE_AT(t));
|
|
1445
|
+
if (seg.slice(from + 1, end < 0 ? seg.length : end).some((t) => t.t === 'sep')) return null;
|
|
1446
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1447
|
+
} else if (st.verb === 'SET') {
|
|
1448
|
+
const down = seg.findIndex((t) => WORD(t, 'DOWN'));
|
|
1449
|
+
if (down < 1 || !WORD(seg[down + 1], 'BY') || seg.length !== down + 3 || seg.slice(0, down).some((t) => t.t !== 'word')) return null;
|
|
1450
|
+
max = amount(seg[down + 2]);
|
|
1451
|
+
if (max == null) return null;
|
|
1452
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1453
|
+
} else if (st.verb === 'COMPUTE') {
|
|
1454
|
+
const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
|
|
1455
|
+
const lhs = seg.slice(0, eq).filter((t) => !WORD(t, 'ROUNDED'));
|
|
1456
|
+
const end = seg.findIndex((t, i) => i > eq && PHRASE_AT(t));
|
|
1457
|
+
const rhs = seg.slice(eq + 1, end < 0 ? seg.length : end);
|
|
1458
|
+
if (eq < 1 || lhs.length !== 1 || lhs[0].t !== 'word' || rhs.length !== 3 || rhs[1].t !== 'op' || rhs[1].v !== '-') return null;
|
|
1459
|
+
const target = resolve(lhs[0]);
|
|
1460
|
+
if (rhs[0].t !== 'word' || resolve(rhs[0]) !== target) return null;
|
|
1461
|
+
max = amount(rhs[2]);
|
|
1462
|
+
if (max == null) return null;
|
|
1463
|
+
fields = [target];
|
|
1464
|
+
} else return null;
|
|
1465
|
+
fields = [...new Set(fields.filter((f) => intCap(f) != null))];
|
|
1466
|
+
return fields.length ? { fields, max } : null;
|
|
1467
|
+
};
|
|
1422
1468
|
const lifted = new Map();
|
|
1423
1469
|
for (const [id, inc] of raisers) {
|
|
1424
1470
|
const list = [];
|
|
@@ -1712,7 +1758,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1712
1758
|
const r = rangeOfField(leaf.f);
|
|
1713
1759
|
const base = { needs: [], lo: r.hold.lo, hi: r.hold.hi };
|
|
1714
1760
|
if (!leaf.plain) return [base];
|
|
1715
|
-
const cv = numberOf(constValue([leaf.tok]));
|
|
1761
|
+
const cv = leaf.tok ? numberOf(constValue([leaf.tok])) : null;
|
|
1716
1762
|
if (cv != null && Number.isInteger(cv)) return [{ needs: [], lo: BigInt(cv), hi: BigInt(cv) }];
|
|
1717
1763
|
if (!heldBits) return [base];
|
|
1718
1764
|
let lo = null;
|
|
@@ -1729,7 +1775,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1729
1775
|
const needs = [...new Set([lo && lo.bit, hi && hi.bit].filter((x) => x != null))].sort((x, y) => x - y);
|
|
1730
1776
|
const from = lo ? lo.v : base.lo;
|
|
1731
1777
|
const to = hi ? hi.v : base.hi;
|
|
1732
|
-
|
|
1778
|
+
if (!needs.length || from > to) return [base];
|
|
1779
|
+
// Each end alone as well, so a result that needs only one of them does not wait on the other.
|
|
1780
|
+
const ends = lo && hi && lo.bit !== hi.bit ? [{ needs: [lo.bit], lo: lo.v, hi: base.hi }, { needs: [hi.bit], lo: base.lo, hi: hi.v }] : [];
|
|
1781
|
+
return [base, { needs, lo: from, hi: to }, ...ends];
|
|
1733
1782
|
};
|
|
1734
1783
|
// A fact "the field holds a whole number from lo to hi" (either end may be open), one per field and interval.
|
|
1735
1784
|
const derivedOf = new Map();
|
|
@@ -1769,6 +1818,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1769
1818
|
// is not among them: the interval is what the data holds, not a test that ran.
|
|
1770
1819
|
const withLooser = (fact) => {
|
|
1771
1820
|
const out = [fact.t];
|
|
1821
|
+
if (fact.pair) {
|
|
1822
|
+
for (const [k, other] of pairFacts.get(fact.pairKey)) if (k > fact.k && (fact.pair === 'span' || other.a === fact.a)) out.push(other.t);
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1772
1825
|
const own = fact.bounds;
|
|
1773
1826
|
for (const c of factsByKey.get(fact.key) || []) {
|
|
1774
1827
|
if (c === fact || !c.derived) continue;
|
|
@@ -1781,9 +1834,19 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1781
1834
|
}
|
|
1782
1835
|
return out;
|
|
1783
1836
|
};
|
|
1837
|
+
// The derived intervals that hold every whole number from lo to hi, an open end holding nothing.
|
|
1838
|
+
const containing = (key, lo, hi) => {
|
|
1839
|
+
const out = [];
|
|
1840
|
+
for (const c of factsByKey.get(key) || []) {
|
|
1841
|
+
if (!c.derived) continue;
|
|
1842
|
+
const e = wholeEnds(c.tCons);
|
|
1843
|
+
if (e && (e.lo == null || (lo != null && e.lo <= lo)) && (e.hi == null || (hi != null && e.hi >= hi))) out.push(c.t);
|
|
1844
|
+
}
|
|
1845
|
+
return out;
|
|
1846
|
+
};
|
|
1784
1847
|
const expandRules = () => {
|
|
1785
1848
|
indexFacts();
|
|
1786
|
-
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; }
|
|
1849
|
+
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; n.genArithOn = undefined; }
|
|
1787
1850
|
for (const r of rules) {
|
|
1788
1851
|
const bits = withLooser(r.fact);
|
|
1789
1852
|
const at = r.bucket == null ? r.n : nodes[r.n.test];
|
|
@@ -1792,6 +1855,26 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1792
1855
|
else ((at.genArithOn ||= { true: [], false: [] })[r.bucket]).push(...bits);
|
|
1793
1856
|
} else (r.n.derive ||= []).push({ needs: r.needs, to: bits });
|
|
1794
1857
|
}
|
|
1858
|
+
// A constant moved, and a check's outcome, are values too: routes that reached an interval by
|
|
1859
|
+
// arithmetic and routes that reached it by a move or a test meet at it.
|
|
1860
|
+
for (const a of assigns) {
|
|
1861
|
+
const v = numberOf(a.value);
|
|
1862
|
+
if (v == null || !Number.isInteger(v)) continue;
|
|
1863
|
+
const bits = containing(fieldKey(a.field), BigInt(v), BigInt(v));
|
|
1864
|
+
if (bits.length) (a.n.genArith ||= []).push(...bits);
|
|
1865
|
+
}
|
|
1866
|
+
for (const id of tests) {
|
|
1867
|
+
const n = nodes[id];
|
|
1868
|
+
for (const c of n.checks || []) {
|
|
1869
|
+
for (const [cons, bucket] of [[c.tCons, 'true'], [c.fCons, 'false']]) {
|
|
1870
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
1871
|
+
const e = wholeEnds(cons);
|
|
1872
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
1873
|
+
const bits = containing(c.key, e.lo, e.hi);
|
|
1874
|
+
if (bits.length) ((n.genArithOn ||= { true: [], false: [] })[bucket]).push(...bits);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1795
1878
|
};
|
|
1796
1879
|
|
|
1797
1880
|
const ARITH_VERBS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'MOVE']);
|
|
@@ -1971,6 +2054,108 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1971
2054
|
relations.clear();
|
|
1972
2055
|
}
|
|
1973
2056
|
|
|
2057
|
+
// Facts over two whole-number fields, killed by a write to either: "A - B <= k" (an order) and
|
|
2058
|
+
// "A + B <= k" (a span, which keeps a reference modification X(A:B) inside X). They are drawn
|
|
2059
|
+
// only for the start and length of a reference modification and the fields a length is computed from.
|
|
2060
|
+
const spanPairs = new Map();
|
|
2061
|
+
for (const st of prog.statements) {
|
|
2062
|
+
for (const x of st.indexes || []) {
|
|
2063
|
+
if (x.kind !== 'refmod-length' || !x.from || !x.from.tok) continue;
|
|
2064
|
+
const len = x.tok && x.tok.t === 'word' ? resolve(x.tok) : null;
|
|
2065
|
+
const start = resolve(x.from.tok);
|
|
2066
|
+
if (!len || !start || len === start || !rangeOfField(len) || !rangeOfField(start) || inTable(len) || inTable(start)) continue;
|
|
2067
|
+
if (!spanPairs.has(len)) spanPairs.set(len, new Set());
|
|
2068
|
+
spanPairs.get(len).add(start);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
const starts = new Set([...spanPairs.values()].flatMap((s) => [...s]));
|
|
2072
|
+
const pairIds = new Map();
|
|
2073
|
+
const pairId = (f) => { if (!pairIds.has(f)) pairIds.set(f, pairIds.size); return pairIds.get(f); };
|
|
2074
|
+
const pairFacts = new Map();
|
|
2075
|
+
const pairKey = (kind, a, b) => (kind === 'span' && pairId(a) > pairId(b) ? `${kind}|${pairId(b)}|${pairId(a)}` : `${kind}|${pairId(a)}|${pairId(b)}`);
|
|
2076
|
+
const pairFact = (kind, a, b, k, at) => {
|
|
2077
|
+
const key = pairKey(kind, a, b);
|
|
2078
|
+
if (!pairFacts.has(key)) pairFacts.set(key, new Map());
|
|
2079
|
+
const mine = pairFacts.get(key);
|
|
2080
|
+
if (mine.has(k)) return mine.get(k);
|
|
2081
|
+
if (!arithBudget()) return null;
|
|
2082
|
+
const fact = { pair: kind, pairKey: key, a, b, k, t: nFacts++, file: at.file, line: at.line };
|
|
2083
|
+
for (const f of [a, b]) {
|
|
2084
|
+
const fk = fieldKey(f);
|
|
2085
|
+
noteField(fk, f);
|
|
2086
|
+
checks.push({ pseudo: true, key: fk, field: f, x: null, t: fact.t, f: null });
|
|
2087
|
+
}
|
|
2088
|
+
mine.set(k, fact);
|
|
2089
|
+
return fact;
|
|
2090
|
+
};
|
|
2091
|
+
// What an outcome makes true of two fields, as order and span facts: S <= E, S + L - 1 <= 80.
|
|
2092
|
+
const pairsOf = (rel, pol) => {
|
|
2093
|
+
const op = pol ? rel.op : NEGATE[rel.op];
|
|
2094
|
+
if (!['<', '<=', '>', '>=', '='].includes(op)) return [];
|
|
2095
|
+
const l = linearOf(rel.left), r = linearOf(rel.right);
|
|
2096
|
+
const e = l && r ? linAdd(l, r, -1) : null;
|
|
2097
|
+
if (!e || e.terms.size !== 2 || e.lo !== e.hi || !Number.isInteger(e.lo)) return [];
|
|
2098
|
+
const out = [];
|
|
2099
|
+
// e op 0 as one or two facts "sum <= k" over the terms, each with its signs.
|
|
2100
|
+
const below = (sign, strict) => {
|
|
2101
|
+
const terms = [...e.terms].map(([f, c]) => [f, sign * c]);
|
|
2102
|
+
if (terms.some(([f, c]) => Math.abs(c) !== 1 || !rangeOfField(f) || inTable(f))) return;
|
|
2103
|
+
const k = -sign * e.lo - (strict ? 1 : 0);
|
|
2104
|
+
const [[a, ca], [b, cb]] = terms;
|
|
2105
|
+
if (ca > 0 && cb > 0) { if ((spanPairs.get(a)?.has(b)) || (spanPairs.get(b)?.has(a))) out.push(['span', a, b, k]); }
|
|
2106
|
+
else if (ca > 0 && cb < 0) { if (starts.has(a)) out.push(['order', a, b, k]); }
|
|
2107
|
+
else if (ca < 0 && cb > 0) { if (starts.has(b)) out.push(['order', b, a, k]); }
|
|
2108
|
+
};
|
|
2109
|
+
if (op === '<' || op === '<=' || op === '=') below(1, op === '<');
|
|
2110
|
+
if (op === '>' || op === '>=' || op === '=') below(-1, op === '>');
|
|
2111
|
+
return out;
|
|
2112
|
+
};
|
|
2113
|
+
if (spanPairs.size) {
|
|
2114
|
+
for (const id of tests) {
|
|
2115
|
+
const n = nodes[id];
|
|
2116
|
+
if (!n.tree) continue;
|
|
2117
|
+
for (const o of [true, false]) {
|
|
2118
|
+
const at = outcomeAt.get(`${id}|${o}`);
|
|
2119
|
+
if (!at) continue;
|
|
2120
|
+
for (const { rel, pol } of relationsOf(n.tree, o)) {
|
|
2121
|
+
for (const [kind, a, b, k] of pairsOf(rel, pol)) {
|
|
2122
|
+
const fact = pairFact(kind, a, b, k, n.at || n.st);
|
|
2123
|
+
if (fact) derive(at, [], fact, String(o));
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
// The statements that only raise a field of a span, by at most `max`.
|
|
2130
|
+
const spanRaisers = [];
|
|
2131
|
+
if (spanPairs.size) {
|
|
2132
|
+
const spanFields = new Set([...spanPairs.keys(), ...starts]);
|
|
2133
|
+
nodes.forEach((n, id) => {
|
|
2134
|
+
if (n.kind !== 'stmt' || !n.st || n.st.at == null) return;
|
|
2135
|
+
const inc = increments(n.st);
|
|
2136
|
+
if (inc && inc.fields.some((f) => spanFields.has(f))) spanRaisers.push({ id, inc, wrote: writtenBy(n) });
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
const spanLifted = new Set();
|
|
2140
|
+
// An arithmetic tree as whole-number coefficients per plain field plus a whole constant, or null.
|
|
2141
|
+
const linearTree = (t) => {
|
|
2142
|
+
switch (t.k) {
|
|
2143
|
+
case 'const': return t.v.d === 1n ? { terms: new Map(), c: Number(t.v.n) } : null;
|
|
2144
|
+
case 'field': return t.plain ? { terms: new Map([[t.f, 1]]), c: 0 } : null;
|
|
2145
|
+
case 'neg': { const a = linearTree(t.a); return a && { terms: new Map([...a.terms].map(([f, c]) => [f, -c])), c: -a.c }; }
|
|
2146
|
+
case 'bin': {
|
|
2147
|
+
if (t.op !== '+' && t.op !== '-') return null;
|
|
2148
|
+
const a = linearTree(t.a), b = linearTree(t.b);
|
|
2149
|
+
if (!a || !b) return null;
|
|
2150
|
+
const sign = t.op === '+' ? 1 : -1;
|
|
2151
|
+
const terms = new Map(a.terms);
|
|
2152
|
+
for (const [f, c] of b.terms) { const v = (terms.get(f) || 0) + sign * c; if (v) terms.set(f, v); else terms.delete(f); }
|
|
2153
|
+
return { terms, c: a.c + sign * b.c };
|
|
2154
|
+
}
|
|
2155
|
+
default: return null;
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
|
|
1974
2159
|
// The fields that matter: those an index reads, and those that flow into them through the
|
|
1975
2160
|
// statements above and the relations between fields.
|
|
1976
2161
|
const relevant = new Set();
|
|
@@ -1985,6 +2170,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1985
2170
|
const before = relevant.size;
|
|
1986
2171
|
for (const a of arith) if (a.targets.some((r) => r.f && relevant.has(r.f))) for (const tree of a.trees) for (const l of tree ? leaves(tree) : []) relevant.add(l.f);
|
|
1987
2172
|
for (const r of relations.values()) if (relevant.has(r.f) || relevant.has(r.g)) { relevant.add(r.f); relevant.add(r.g); }
|
|
2173
|
+
// A loop condition that bounds a relevant counter through another field makes that field relevant.
|
|
2174
|
+
for (const c of checks) { if (c.pseudo || !c.field || !relevant.has(c.field)) continue; for (const d of [c.tDep, c.fDep]) if (d?.g) relevant.add(d.g); }
|
|
1988
2175
|
more = relevant.size > before;
|
|
1989
2176
|
}
|
|
1990
2177
|
|
|
@@ -1996,20 +2183,77 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1996
2183
|
let grew = false;
|
|
1997
2184
|
try {
|
|
1998
2185
|
indexFacts();
|
|
2186
|
+
// A check's outcome as an interval of its own, which arithmetic's intervals can widen into at a join.
|
|
2187
|
+
for (const id of tests) {
|
|
2188
|
+
for (const c of nodes[id].checks || []) {
|
|
2189
|
+
if (!relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2190
|
+
for (const cons of [c.tCons, c.fCons]) {
|
|
2191
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
2192
|
+
const e = wholeEnds(cons);
|
|
2193
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
2194
|
+
const had = derivedOf.get(c.field)?.size || 0;
|
|
2195
|
+
derivedFact(c.field, e.lo, e.hi, nodes[id].at || nodes[id].st);
|
|
2196
|
+
if ((derivedOf.get(c.field)?.size || 0) > had) grew = true;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
1999
2200
|
for (const a of arith) {
|
|
2000
2201
|
a.trees.forEach((tree, i) => {
|
|
2001
2202
|
const r = a.targets[i];
|
|
2002
2203
|
if (!tree || !r.f || !rangeOfField(r.f) || inTable(r.f) || !relevant.has(r.f)) return;
|
|
2003
|
-
const fit = rangeOfField(r.f)
|
|
2204
|
+
const { fit, hold } = rangeOfField(r.f);
|
|
2004
2205
|
heldBits = prev ? prev.get(a.n.id) || null : null;
|
|
2005
2206
|
for (const c of candidates(tree, leafCands, 64)) {
|
|
2006
2207
|
const s = storedRange(c.iv, r.rounded);
|
|
2007
|
-
if (s.lo
|
|
2008
|
-
|
|
2208
|
+
if (s.lo === fit.lo && s.hi === fit.hi) continue;
|
|
2209
|
+
// A result past one end of what the field keeps loses high digits and keeps its sign, and no
|
|
2210
|
+
// result past what the storage holds is added, so the other end still bounds it.
|
|
2211
|
+
const whole = s.lo >= fit.lo && s.hi <= fit.hi;
|
|
2212
|
+
const top = whole || (s.lo >= hold.lo && s.hi <= fit.hi && (s.hi >= 0n || s.lo >= fit.lo));
|
|
2213
|
+
const bottom = whole || (s.hi <= hold.hi && s.lo >= fit.lo && (s.lo <= 0n || s.hi <= fit.hi));
|
|
2214
|
+
if (!top && !bottom) continue;
|
|
2215
|
+
const fact = derivedFact(r.f, bottom ? s.lo : null, top ? s.hi : null, a.n.st);
|
|
2009
2216
|
if (fact && derive(a.n, c.needs, fact)) grew = true;
|
|
2010
2217
|
}
|
|
2011
2218
|
});
|
|
2012
2219
|
}
|
|
2220
|
+
// A length computed as E - S + c from its start S, where S <= E + d held, is at least c - d, which
|
|
2221
|
+
// intervals alone cannot see; and S + L is then E + c, at most what E could hold plus c.
|
|
2222
|
+
for (const a of arith) {
|
|
2223
|
+
a.trees.forEach((tree, i) => {
|
|
2224
|
+
const r = a.targets[i];
|
|
2225
|
+
const mates = r.f && spanPairs.get(r.f);
|
|
2226
|
+
const lin = mates && tree ? linearTree(tree) : null;
|
|
2227
|
+
if (!lin) return;
|
|
2228
|
+
const fit = rangeOfField(r.f).fit;
|
|
2229
|
+
heldBits = prev ? prev.get(a.n.id) || null : null;
|
|
2230
|
+
for (const [s, cs] of lin.terms) {
|
|
2231
|
+
if (cs !== -1 || !mates.has(s) || s === r.f) continue;
|
|
2232
|
+
const rest = [...lin.terms].filter(([f]) => f !== s);
|
|
2233
|
+
if (rest.length > 1 || (rest.length && (rest[0][1] !== 1 || rest[0][0] === r.f))) continue;
|
|
2234
|
+
const e = rest.length ? rest[0][0] : null;
|
|
2235
|
+
const orders = e ? [...(pairFacts.get(pairKey('order', s, e))?.values() || [])] : [];
|
|
2236
|
+
const eCands = e ? leafCands({ f: e, tok: null, plain: true }) : [{ needs: [], lo: 0n, hi: 0n }];
|
|
2237
|
+
const sCands = leafCands({ f: s, tok: null, plain: true });
|
|
2238
|
+
// With no E, the lowest L is c less S's highest, which an interval of S already gives.
|
|
2239
|
+
const floors = e ? orders.map((o) => ({ needs: [o.t], lo: BigInt(lin.c - o.k) })) : sCands.map((sc) => ({ needs: sc.needs, lo: BigInt(lin.c) - sc.hi }));
|
|
2240
|
+
for (const fl of floors) {
|
|
2241
|
+
if (fl.lo < fit.lo) continue;
|
|
2242
|
+
for (const ec of eCands) {
|
|
2243
|
+
for (const sc of sCands) {
|
|
2244
|
+
const hi = ec.hi - sc.lo + BigInt(lin.c);
|
|
2245
|
+
if (hi > fit.hi || hi < fl.lo) continue;
|
|
2246
|
+
const needs = [...new Set([...fl.needs, ...ec.needs, ...sc.needs])].sort((x, y) => x - y);
|
|
2247
|
+
const fact = derivedFact(r.f, fl.lo, hi, a.n.st);
|
|
2248
|
+
if (fact && derive(a.n, needs, fact)) grew = true;
|
|
2249
|
+
const span = pairFact('span', s, r.f, Number(ec.hi) + lin.c, a.n.st);
|
|
2250
|
+
if (span && derive(a.n, needs, span)) grew = true;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2013
2257
|
for (const rel of relations.values()) {
|
|
2014
2258
|
if (!relevant.has(rel.f) && !relevant.has(rel.g)) continue;
|
|
2015
2259
|
const at = nodes[rel.test].at || nodes[rel.test].st;
|
|
@@ -2032,6 +2276,45 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2032
2276
|
}
|
|
2033
2277
|
}
|
|
2034
2278
|
}
|
|
2279
|
+
// An increment of a span's field by at most n leaves the span n looser: the sum grows by n at most,
|
|
2280
|
+
// and a sum that wraps or is cut short is only lower.
|
|
2281
|
+
for (const { id, inc, wrote } of spanRaisers) {
|
|
2282
|
+
const held = prev ? prev.get(id) : null;
|
|
2283
|
+
if (!held) continue;
|
|
2284
|
+
for (const mine of pairFacts.values()) {
|
|
2285
|
+
for (const f of [...mine.values()]) {
|
|
2286
|
+
if (f.pair !== 'span' || !hasFact(held, f.t)) continue;
|
|
2287
|
+
const count = [f.a, f.b].filter((x) => inc.fields.includes(x)).length;
|
|
2288
|
+
if (!count || wrote.some((w) => !inc.fields.includes(w) && (overlaps(w, f.a) || overlaps(w, f.b)))) continue;
|
|
2289
|
+
const to = pairFact('span', f.a, f.b, f.k + count * inc.max, nodes[id].st);
|
|
2290
|
+
if (!to || spanLifted.has(`${id}|${f.t}|${to.t}`)) continue;
|
|
2291
|
+
spanLifted.add(`${id}|${f.t}|${to.t}`);
|
|
2292
|
+
(nodes[id].spanLifts ||= []).push([f.t, to.t]);
|
|
2293
|
+
grew = true;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
// A bound over an expression in one other field, X <= G - 1, takes the interval G holds at the test.
|
|
2298
|
+
for (const c of checks) {
|
|
2299
|
+
if (c.pseudo || c.test == null || !relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2300
|
+
for (const o of [true, false]) {
|
|
2301
|
+
const dep = o ? c.tDep : c.fDep;
|
|
2302
|
+
const at = dep && outcomeAt.get(`${c.test}|${o}`);
|
|
2303
|
+
if (!at || !rangeOfField(dep.g) || inTable(dep.g) || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) continue;
|
|
2304
|
+
const hold = rangeOfField(c.field).hold;
|
|
2305
|
+
heldBits = prev ? prev.get(c.test) || null : null;
|
|
2306
|
+
for (const cand of leafCands({ f: dep.g, tok: null, plain: true })) {
|
|
2307
|
+
const v = BigInt(dep.m) * (dep.needs === 'hi' ? cand.hi : cand.lo) + BigInt(dep.b);
|
|
2308
|
+
let lo = null, hi = null;
|
|
2309
|
+
if (dep.side === 'hi') hi = dep.inc ? v : v - 1n; else lo = dep.inc ? v : v + 1n;
|
|
2310
|
+
if (lo != null && lo <= hold.lo) lo = null;
|
|
2311
|
+
if (hi != null && hi >= hold.hi) hi = null;
|
|
2312
|
+
if ((lo == null && hi == null) || (hi != null && hi < hold.lo) || (lo != null && lo > hold.hi)) continue;
|
|
2313
|
+
const fact = derivedFact(c.field, lo, hi, at.st || nodes[c.test].at || nodes[c.test].st);
|
|
2314
|
+
if (fact && derive(at, cand.needs, fact, String(o))) grew = true;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2035
2318
|
// A relation holds after the outcome of its test that makes it true, and the fields it bounds hold
|
|
2036
2319
|
// what the other side could hold.
|
|
2037
2320
|
for (const id of tests) {
|
|
@@ -2161,6 +2444,31 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2161
2444
|
} else if (n.kind === 'step' && stepped.has(id)) {
|
|
2162
2445
|
keepAcross(n, [{ ...stepped.get(id), sizeError: false }]);
|
|
2163
2446
|
}
|
|
2447
|
+
// An upper bound survives a decrement that cannot go below 0: one where the field was at least the
|
|
2448
|
+
// amount before it, so nothing is stored as an absolute value or wraps.
|
|
2449
|
+
if (n.kind === 'stmt' && n.st && n.st.at != null) {
|
|
2450
|
+
const dec = decrements(n.st);
|
|
2451
|
+
const wrote = dec ? writtenBy(n) : [];
|
|
2452
|
+
for (const field of dec ? dec.fields : []) {
|
|
2453
|
+
if (wrote.some((w) => w !== field && overlaps(w, field))) continue;
|
|
2454
|
+
const key = fieldKey(field);
|
|
2455
|
+
const keep = [];
|
|
2456
|
+
const guards = [];
|
|
2457
|
+
for (const c of checks) {
|
|
2458
|
+
if (c.key !== key || c.pseudo) continue;
|
|
2459
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2460
|
+
if (bit == null || !cons || cons.dep || (cons.set && cons.set.size)) continue;
|
|
2461
|
+
const r = asRange(cons);
|
|
2462
|
+
if (r.hi != null && r.lo == null) keep.push(bit);
|
|
2463
|
+
if (r.lo != null && (r.loInc ? r.lo : Math.floor(r.lo) + 1) >= dec.max) guards.push(bit);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (!keep.length || !guards.length) continue;
|
|
2467
|
+
const bits = new Uint32Array(words);
|
|
2468
|
+
for (const b of keep) bits[b >>> 5] |= 1 << (b & 31);
|
|
2469
|
+
(n.keeps ||= []).push({ bits, guards });
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2164
2472
|
if (n.kind === 'step' && keptOnStep.has(id)) {
|
|
2165
2473
|
const bits = new Uint32Array(words);
|
|
2166
2474
|
for (const b of keptOnStep.get(id)) bits[b >>> 5] |= 1 << (b & 31);
|
|
@@ -2202,6 +2510,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2202
2510
|
const killScratch = new Uint32Array(words);
|
|
2203
2511
|
// A summary is joined to whatever enters the range, so it cannot assume a guard the caller lacks.
|
|
2204
2512
|
let summarising = false;
|
|
2513
|
+
// While summarising, a fact that depends on others is taken only where the range's own statements
|
|
2514
|
+
// make those others on every route: then it holds whatever the caller brings.
|
|
2515
|
+
let approved = null;
|
|
2516
|
+
const has = (bits, x) => (bits[x >>> 5] & (1 << (x & 31))) !== 0;
|
|
2205
2517
|
// What holds after a node, in a buffer reused by every call: the caller merges it into the next
|
|
2206
2518
|
// node's facts before calling again. Null where the route ends here.
|
|
2207
2519
|
const transfer = (n, input) => {
|
|
@@ -2224,7 +2536,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2224
2536
|
kill = killScratch;
|
|
2225
2537
|
kill.set(n.kill);
|
|
2226
2538
|
for (const k of n.keeps) {
|
|
2227
|
-
if (k.guards && (summarising
|
|
2539
|
+
if (k.guards && (summarising ? !approved.has(k) : !k.guards.some((g) => has(input, g)))) continue;
|
|
2228
2540
|
for (let i = 0; i < words; i++) kill[i] &= ~k.bits[i];
|
|
2229
2541
|
}
|
|
2230
2542
|
}
|
|
@@ -2239,26 +2551,36 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2239
2551
|
// An increment that cannot wrap raises the lower bounds that held before it.
|
|
2240
2552
|
if (n.lifts) {
|
|
2241
2553
|
for (const { pairs, guards } of n.lifts) {
|
|
2242
|
-
if (summarising
|
|
2243
|
-
for (const
|
|
2244
|
-
|
|
2554
|
+
if (!summarising && !guards.some((g) => has(input, g))) continue;
|
|
2555
|
+
for (const pair of pairs) {
|
|
2556
|
+
const [from, to] = pair;
|
|
2557
|
+
if (summarising ? !approved.has(pair) : !has(input, from)) continue;
|
|
2245
2558
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2246
2559
|
out[to >>> 5] |= 1 << (to & 31);
|
|
2247
2560
|
}
|
|
2248
2561
|
}
|
|
2249
2562
|
}
|
|
2563
|
+
if (n.spanLifts) {
|
|
2564
|
+
for (const pair of n.spanLifts) {
|
|
2565
|
+
const [from, to] = pair;
|
|
2566
|
+
if (summarising ? !approved.has(pair) : !has(input, from)) continue;
|
|
2567
|
+
if (out === input) { scratch.set(input); out = scratch; }
|
|
2568
|
+
out[to >>> 5] |= 1 << (to & 31);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2250
2571
|
// What a statement or a test leaves in a field, where the facts its operands needed held before it.
|
|
2251
|
-
if (n.derive
|
|
2572
|
+
if (n.derive) {
|
|
2252
2573
|
for (const d of n.derive) {
|
|
2253
|
-
if (!d.needs.every((x) => input
|
|
2574
|
+
if (summarising ? !approved.has(d) : !d.needs.every((x) => has(input, x))) continue;
|
|
2254
2575
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2255
2576
|
for (const t of d.to) out[t >>> 5] |= 1 << (t & 31);
|
|
2256
2577
|
}
|
|
2257
2578
|
}
|
|
2258
2579
|
// A flag test's outcome turns what held before it into the bound the flag stood for.
|
|
2259
2580
|
if (n.implies) {
|
|
2260
|
-
for (const
|
|
2261
|
-
|
|
2581
|
+
for (const entry of n.implies) {
|
|
2582
|
+
const [from, to, soft] = entry;
|
|
2583
|
+
if (soft && summarising && !approved.has(entry)) continue;
|
|
2262
2584
|
if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
|
|
2263
2585
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
2264
2586
|
out[to >>> 5] |= 1 << (to & 31);
|
|
@@ -2339,8 +2661,22 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2339
2661
|
const r = pending.shift();
|
|
2340
2662
|
queued.delete(r.key);
|
|
2341
2663
|
const calls = new Set();
|
|
2664
|
+
// From no facts, every fact the run holds was made inside the range, so what depends on it is sound as it stands.
|
|
2665
|
+
summarising = false;
|
|
2666
|
+
const fromNone = run(r.entry, ZERO, r.end);
|
|
2667
|
+
summarising = true;
|
|
2668
|
+
const bot = fromNone.exitOut;
|
|
2669
|
+
approved = new Set();
|
|
2670
|
+
for (const [id, bits] of fromNone.IN) {
|
|
2671
|
+
const n = nodes[id];
|
|
2672
|
+
for (const k of n.keeps || []) if (k.guards && k.guards.some((g) => has(bits, g))) approved.add(k);
|
|
2673
|
+
for (const { pairs, guards } of n.lifts || []) if (guards.some((g) => has(bits, g))) for (const pair of pairs) if (has(bits, pair[0])) approved.add(pair);
|
|
2674
|
+
for (const pair of n.spanLifts || []) if (has(bits, pair[0])) approved.add(pair);
|
|
2675
|
+
for (const d of n.derive || []) if (d.needs.every((x) => has(bits, x))) approved.add(d);
|
|
2676
|
+
for (const entry of n.implies || []) if (entry[2] && has(bits, entry[0])) approved.add(entry);
|
|
2677
|
+
}
|
|
2342
2678
|
const top = run(r.entry, ONES, r.end, calls).exitOut;
|
|
2343
|
-
|
|
2679
|
+
approved = null;
|
|
2344
2680
|
for (const k of calls) { if (!callers.has(k)) callers.set(k, new Set()); callers.get(k).add(r); }
|
|
2345
2681
|
const old = summaries.get(r.key);
|
|
2346
2682
|
const nextS = top && bot ? { top, bot } : { top: null, bot: null };
|
|
@@ -2440,6 +2776,39 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2440
2776
|
const posOf = new Map();
|
|
2441
2777
|
const places = [];
|
|
2442
2778
|
let placeFacts = nFacts;
|
|
2779
|
+
// The tightest end of a field's value the facts `bits` hold, or its picture's.
|
|
2780
|
+
const boundAt = (g, needs, bits) => {
|
|
2781
|
+
const hold = rangeOfField(g)?.hold;
|
|
2782
|
+
let best = hold ? Number(needs === 'hi' ? hold.hi : hold.lo) : null;
|
|
2783
|
+
if (best != null && !Number.isFinite(best)) best = null;
|
|
2784
|
+
for (const c of checks) {
|
|
2785
|
+
if (c.pseudo || c.key !== fieldKey(g)) continue;
|
|
2786
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2787
|
+
if (!cons || !hasFact(bits, bit)) continue;
|
|
2788
|
+
const e = wholeEnds(cons);
|
|
2789
|
+
const v = e && (needs === 'hi' ? e.hi : e.lo);
|
|
2790
|
+
if (v == null) continue;
|
|
2791
|
+
const n = Number(v);
|
|
2792
|
+
if (best == null || (needs === 'hi' ? n < best : n > best)) best = n;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return best;
|
|
2796
|
+
};
|
|
2797
|
+
// A relation over an expression, X <= G - 1, read with what G held at the test.
|
|
2798
|
+
const resolveDeps = (map, bits) => {
|
|
2799
|
+
const out = new Map();
|
|
2800
|
+
for (const [key, c] of map) {
|
|
2801
|
+
const dep = c && c.dep;
|
|
2802
|
+
const g = dep ? boundAt(dep.g, dep.needs, bits) : null;
|
|
2803
|
+
if (g == null || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) { out.set(key, c); continue; }
|
|
2804
|
+
const v = dep.m * g + dep.b;
|
|
2805
|
+
const plain = plainCons(c) || {};
|
|
2806
|
+
const had = plain[dep.side];
|
|
2807
|
+
const tighter = had == null || (dep.side === 'hi' ? v < had : v > had);
|
|
2808
|
+
out.set(key, tighter ? { ...plain, [dep.side]: v, [`${dep.side}Inc`]: dep.inc } : plain);
|
|
2809
|
+
}
|
|
2810
|
+
return out;
|
|
2811
|
+
};
|
|
2443
2812
|
for (const id of tests) {
|
|
2444
2813
|
const n = nodes[id];
|
|
2445
2814
|
if (!n.st || !n.st.indexes || !n.tree || n.evaluate || !(n.st.verb === 'IF' || n.loop)) continue;
|
|
@@ -2454,7 +2823,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2454
2823
|
for (const g of guards || []) {
|
|
2455
2824
|
let cons = new Map();
|
|
2456
2825
|
const r = new Map();
|
|
2457
|
-
|
|
2826
|
+
const atTest = facts.get(id);
|
|
2827
|
+
for (const [sub, outcome] of g) { cons = meetMaps(cons, atTest ? resolveDeps(consOf(sub, outcome), atTest) : consOf(sub, outcome)); testedWhen(sub, outcome, r); }
|
|
2458
2828
|
said = said ? joinMaps(said, cons) : cons;
|
|
2459
2829
|
ran = ran ? new Map([...ran].filter(([k]) => r.has(k))) : r;
|
|
2460
2830
|
}
|
|
@@ -2501,7 +2871,20 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2501
2871
|
// the caller has let the parse tree go. A statement no context reaches has no facts, and credits
|
|
2502
2872
|
// nothing. One `reached` does not mark does not run, unless `partial` says the analysis stopped
|
|
2503
2873
|
// short or met a paragraph the parse did not find.
|
|
2504
|
-
|
|
2874
|
+
const spans = [];
|
|
2875
|
+
for (const mine of pairFacts.values()) for (const f of mine.values()) if (f.pair === 'span') spans.push({ a: f.a, b: f.b, k: f.k, t: f.t });
|
|
2876
|
+
return { loopBody, nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), spans, words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// The most the facts at a point let a node's value be, as a whole number, or null for no upper end.
|
|
2880
|
+
export function topOf(checks, bits) {
|
|
2881
|
+
let cons = null;
|
|
2882
|
+
for (const c of checks || []) for (const [bit, said] of [[c.t, c.tCons], [c.f, c.fCons]]) if (hasFact(bits, bit)) cons = meet(cons, said);
|
|
2883
|
+
return cons ? wholeTop(cons) : null;
|
|
2884
|
+
}
|
|
2885
|
+
export function wholeTop(said) {
|
|
2886
|
+
const r = asRange(said);
|
|
2887
|
+
return r.hi == null ? null : r.hiInc ? Math.floor(r.hi) : Math.ceil(r.hi) - 1;
|
|
2505
2888
|
}
|
|
2506
2889
|
|
|
2507
2890
|
// For each token a condition reads, what each reading of it waits on: the operands to its left at
|