@portll/cobolwork 0.2.117 → 0.2.150
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 +13 -1
- 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/cics-commands.mjs +136 -125
- package/lib/compliance.mjs +6 -5
- package/lib/control.mjs +295 -6
- package/lib/dataflow.mjs +72 -88
- package/lib/diff.mjs +65 -89
- package/lib/embedded-sql.mjs +110 -0
- package/lib/equivalence.mjs +30 -12
- package/lib/evidence/journal.mjs +11 -2
- package/lib/evidence/record.mjs +3 -1
- package/lib/evidence/slsa.mjs +17 -2
- package/lib/execution.mjs +77 -0
- package/lib/explain.mjs +5 -4
- package/lib/ftp.mjs +136 -0
- package/lib/inventory.mjs +8 -9
- package/lib/ironwork.mjs +4 -6
- package/lib/jcl.mjs +29 -54
- package/lib/kernel/git.mjs +60 -0
- package/lib/kernel/identity.mjs +7 -6
- package/lib/kernel/source-tree.mjs +204 -34
- package/lib/layout.mjs +202 -0
- package/lib/parser.mjs +58 -207
- package/lib/precompile.mjs +5 -88
- package/lib/revision.json +1 -1
- package/lib/sbom.mjs +125 -13
- package/lib/scan.mjs +11 -4
- package/lib/sets/build.mjs +1 -1
- package/lib/sets/cics.mjs +32 -5
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +17 -120
- package/lib/sets/priv.mjs +1 -1
- package/lib/sets/recon.mjs +3 -2
- package/lib/sets/semantics.mjs +1 -1
- package/lib/sets/vendor.mjs +1 -1
- package/lib/sets/zowe.mjs +34 -8
- package/lib/site.mjs +17 -7
- package/lib/sources.mjs +82 -38
- package/lib/utilities.mjs +146 -4
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +2438 -0
- package/rules/compliance-dora.json +1 -1
- package/rules/compliance-ffiec.json +1 -1
- package/rules/compliance-nist80053.json +1 -1
- package/schema/cobolwork.policy.schema.json +94 -17
package/lib/compliance.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// against each. The difference between them is this file, and it is the difference between a
|
|
6
6
|
// tooling budget and a compliance budget.
|
|
7
7
|
//
|
|
8
|
-
// DORA, NIST SP 800-53 and the FFIEC IT Examination Handbook ship
|
|
9
|
-
// may be reproduced: one is EU law, the other two US Government works.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// DORA, NIST SP 800-53 and the FFIEC IT Examination Handbook ship with quotes because their
|
|
9
|
+
// instruments may be reproduced: one is EU law, the other two US Government works. COBIT 2019 ships
|
|
10
|
+
// as identifiers with this project's own rationale and no ISACA text. PCI DSS may not be
|
|
11
|
+
// redistributed and is not mapped.
|
|
12
12
|
//
|
|
13
13
|
// A framework may decline to cover a rule. Neither NIST nor FFIEC has a control that genuinely
|
|
14
14
|
// covers committing an LPAR name to a repository, and that is recorded as an unmapped rule with a
|
|
@@ -19,6 +19,7 @@ const FRAMEWORKS = [
|
|
|
19
19
|
{ id: 'dora', file: '../rules/compliance-dora.json' },
|
|
20
20
|
{ id: 'nist-800-53r5', file: '../rules/compliance-nist80053.json' },
|
|
21
21
|
{ id: 'ffiec', file: '../rules/compliance-ffiec.json' },
|
|
22
|
+
{ id: 'cobit-2019', file: '../rules/compliance-cobit2019.json' },
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
// A framework that will not parse is a framework this scan cannot map against, which is the same
|
|
@@ -64,7 +65,7 @@ const byRule = new Map();
|
|
|
64
65
|
for (const d of loaded) {
|
|
65
66
|
for (const r of d.rows) {
|
|
66
67
|
if (!byRule.has(r.ruleId)) byRule.set(r.ruleId, []);
|
|
67
|
-
byRule.get(r.ruleId).push({ framework: r.framework, clause: r.clause, title: r.title });
|
|
68
|
+
byRule.get(r.ruleId).push({ framework: r.framework, clause: r.clause, ...(r.title ? { title: r.title } : {}) });
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
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));
|
|
@@ -574,7 +580,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
574
580
|
const body = n.k === 'perform' ? add('call', { st: n.st, range: rangeOf(n.target, n.thru), succ: [step] }) : lowerList(n.body, step, loopCtx);
|
|
575
581
|
nodes[t].succ = [outcome(t, true, next), outcome(t, false, body)];
|
|
576
582
|
nodes[head].succ = [n.testAfter ? body : t];
|
|
577
|
-
if (n.st.loops) loopHeads.push({ head, step, loops: n.st.loops });
|
|
583
|
+
if (n.st.loops) loopHeads.push({ head, step, loops: n.st.loops, test: t, testAfter: n.testAfter, body: n.k === 'inline' ? n.body : null });
|
|
578
584
|
return head;
|
|
579
585
|
}
|
|
580
586
|
case 'goto': {
|
|
@@ -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 = [];
|
|
@@ -1486,6 +1532,118 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1486
1532
|
// so the pair is joined at whichever of the two checks comes last.
|
|
1487
1533
|
const outcomeAt = new Map();
|
|
1488
1534
|
for (const node of nodes) if (node.kind === 'outcome') outcomeAt.set(`${node.test}|${node.value}`, node);
|
|
1535
|
+
// A backward scan for the last byte past the spaces, VARYING I FROM the field's length BY -1 UNTIL
|
|
1536
|
+
// I < 1 OR F(I:1) > SPACE, leaves I from 1 to that length where F > SPACES held as it began: a
|
|
1537
|
+
// field that compares above spaces has a byte above a space, under any collating sequence, and the
|
|
1538
|
+
// scan meets it before 0. The witness dies with a write to F or to anything its subscript reads.
|
|
1539
|
+
const keptOnStep = new Map();
|
|
1540
|
+
const isSpaces = (toks) => toks.length === 1 && (WORD(toks[0], 'SPACE') || WORD(toks[0], 'SPACES') || (toks[0].t === 'lit' && !toks[0].prefix && /^ +$/.test(String(toks[0].v))));
|
|
1541
|
+
const textOf = (toks) => toks.map((t) => (t.t === 'word' ? t.u : String(t.v))).join(' ');
|
|
1542
|
+
// A whole alphanumeric item or table element, with the fields its subscript reads.
|
|
1543
|
+
const scanned = (toks) => {
|
|
1544
|
+
const base = toks[0] && toks[0].t === 'word' ? resolve(toks[0]) : null;
|
|
1545
|
+
if (!base || base.index || base.level === 88 || base.size == null || categoryOf(base)?.k !== 'A' || variableLength(base)) return null;
|
|
1546
|
+
let i = 1;
|
|
1547
|
+
while (i + 1 < toks.length && (WORD(toks[i], 'OF') || WORD(toks[i], 'IN')) && toks[i + 1].t === 'word') i += 2;
|
|
1548
|
+
const reads = [];
|
|
1549
|
+
if (i < toks.length) {
|
|
1550
|
+
if (toks[i].v !== '(' || toks[toks.length - 1].v !== ')') return null;
|
|
1551
|
+
let depth = 0;
|
|
1552
|
+
for (let j = i; j < toks.length; j++) {
|
|
1553
|
+
const t = toks[j];
|
|
1554
|
+
if (t.v === ':') return null;
|
|
1555
|
+
if (t.v === '(' || t.v === ')') { depth += t.v === '(' ? 1 : -1; if (!depth && j < toks.length - 1) return null; continue; }
|
|
1556
|
+
const f = t.t === 'word' ? resolve(t) : null;
|
|
1557
|
+
if (f) reads.push(f.level === 88 && f.parent ? f.parent : f);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return { base, reads };
|
|
1561
|
+
};
|
|
1562
|
+
// What an outcome says is above spaces (`above`) or only not all spaces.
|
|
1563
|
+
const notBlank = (tree, outcome, out = []) => {
|
|
1564
|
+
if (tree.k === 'not') return notBlank(tree.a, !outcome, out);
|
|
1565
|
+
if ((tree.k === 'and' && outcome) || (tree.k === 'or' && !outcome)) { notBlank(tree.a, outcome, out); notBlank(tree.b, outcome, out); return out; }
|
|
1566
|
+
if (tree.k !== 'rel') return out;
|
|
1567
|
+
const op = outcome ? tree.op : NEGATE[tree.op];
|
|
1568
|
+
if (isSpaces(tree.right) && (op === '>' || op === '<>')) out.push({ toks: tree.left, above: op === '>' });
|
|
1569
|
+
else if (isSpaces(tree.left) && (op === '<' || op === '<>')) out.push({ toks: tree.right, above: op === '<' });
|
|
1570
|
+
return out;
|
|
1571
|
+
};
|
|
1572
|
+
const scans = [];
|
|
1573
|
+
for (const { head, step, test, testAfter, body, loops } of loopHeads) {
|
|
1574
|
+
if (testAfter || !body || loops.length !== 1 || !body.every((el) => el.k === 'stmt' && el.st.verb === 'CONTINUE')) continue;
|
|
1575
|
+
const loop = loops[0];
|
|
1576
|
+
const counter = loop.counter ? resolve(loop.counter) : null;
|
|
1577
|
+
const cap = intCap(counter);
|
|
1578
|
+
const tree = nodes[test].tree;
|
|
1579
|
+
if (cap == null || !tree || tree.k !== 'or' || !loop.by || loop.by.map((t) => t.v).join('') !== '-1') continue;
|
|
1580
|
+
const from = loop.from || [];
|
|
1581
|
+
let start = numberOf(constValue(from));
|
|
1582
|
+
if (start == null && WORD(from[0], 'LENGTH') && WORD(from[1], 'OF')) start = scanned(from.slice(2))?.base.size ?? null;
|
|
1583
|
+
if (start == null || !Number.isInteger(start) || start < 1 || start > cap) continue;
|
|
1584
|
+
const isCounter = (toks) => toks.length === 1 && toks[0].t === 'word' && resolve(toks[0]) === counter;
|
|
1585
|
+
const done = (r) => r.k === 'rel' && isCounter(r.left) && (r.op === '<' ? numberOf(constValue(r.right)) === 1 : (r.op === '<=' || r.op === '=') && numberOf(constValue(r.right)) === 0);
|
|
1586
|
+
const probe = (r) => {
|
|
1587
|
+
if (r.k !== 'rel') return null;
|
|
1588
|
+
const [el, op] = isSpaces(r.right) ? [r.left, r.op] : isSpaces(r.left) ? [r.right, FLIP[r.op]] : [null, null];
|
|
1589
|
+
if (!el || (op !== '>' && op !== '<>') || el.length < 6) return null;
|
|
1590
|
+
const tail = el.slice(-5);
|
|
1591
|
+
if (tail[0].v !== '(' || !isCounter([tail[1]]) || tail[2].v !== ':' || numberOf(constValue([tail[3]])) !== 1 || tail[4].v !== ')') return null;
|
|
1592
|
+
const toks = el.slice(0, -5);
|
|
1593
|
+
const s = scanned(toks);
|
|
1594
|
+
return s && s.base.size === start ? { toks, above: op === '>' } : null;
|
|
1595
|
+
};
|
|
1596
|
+
const p = done(tree.a) ? probe(tree.b) : done(tree.b) ? probe(tree.a) : null;
|
|
1597
|
+
const exit = p && outcomeAt.get(`${test}|true`);
|
|
1598
|
+
if (exit) scans.push({ ...p, counter, start, exit, head, step, at: loop.counter });
|
|
1599
|
+
}
|
|
1600
|
+
// The scan's counter never passes its start: the step runs only once I < 1 has failed, so it
|
|
1601
|
+
// cannot wrap, and the body writes nothing.
|
|
1602
|
+
for (const s of scans) {
|
|
1603
|
+
const key = fieldKey(s.counter);
|
|
1604
|
+
noteField(key, s.counter);
|
|
1605
|
+
const h = { id: checks.length, key, field: s.counter, file: s.at.file, line: s.at.line, tCons: { hi: s.start, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null };
|
|
1606
|
+
checks.push(h);
|
|
1607
|
+
nodes[s.head].genStmt = [...(nodes[s.head].genStmt || []), h.t];
|
|
1608
|
+
keptOnStep.set(s.step, [...(keptOnStep.get(s.step) || []), h.t]);
|
|
1609
|
+
}
|
|
1610
|
+
if (scans.length) {
|
|
1611
|
+
const witness = new Map();
|
|
1612
|
+
const bitFor = (toks, above) => {
|
|
1613
|
+
const k = `${above ? '>' : '<>'}|${textOf(toks)}`;
|
|
1614
|
+
if (witness.has(k)) return witness.get(k);
|
|
1615
|
+
const s = scanned(toks);
|
|
1616
|
+
const bit = nFacts++;
|
|
1617
|
+
for (const f of [s.base, ...s.reads]) {
|
|
1618
|
+
const key = fieldKey(f);
|
|
1619
|
+
noteField(key, f);
|
|
1620
|
+
checks.push({ pseudo: true, key, field: f, x: null, t: bit, f: null });
|
|
1621
|
+
}
|
|
1622
|
+
witness.set(k, bit);
|
|
1623
|
+
return bit;
|
|
1624
|
+
};
|
|
1625
|
+
const wanted = new Set(scans.map((s) => textOf(s.toks)));
|
|
1626
|
+
for (const id of tests) {
|
|
1627
|
+
const n = nodes[id];
|
|
1628
|
+
if (!n.tree) continue;
|
|
1629
|
+
for (const outcome of [true, false]) {
|
|
1630
|
+
for (const nb of notBlank(n.tree, outcome)) {
|
|
1631
|
+
if (!wanted.has(textOf(nb.toks)) || !scanned(nb.toks)) continue;
|
|
1632
|
+
n.gen[String(outcome)].push(bitFor(nb.toks, false));
|
|
1633
|
+
if (nb.above) n.gen[String(outcome)].push(bitFor(nb.toks, true));
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
for (const s of scans) {
|
|
1638
|
+
const bit = witness.get(`${s.above ? '>' : '<>'}|${textOf(s.toks)}`);
|
|
1639
|
+
if (bit == null) continue;
|
|
1640
|
+
const key = fieldKey(s.counter);
|
|
1641
|
+
noteField(key, s.counter);
|
|
1642
|
+
const y = { id: checks.length, key, field: s.counter, file: s.at.file, line: s.at.line, tCons: { numeric: true, lo: 1, loInc: true, hi: s.start, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds: { lo: 1, hi: s.start } };
|
|
1643
|
+
checks.push(y);
|
|
1644
|
+
(s.exit.implies ||= []).push([bit, y.t]);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1489
1647
|
const MAX_BOUNDS_PER_RELATION = 8;
|
|
1490
1648
|
for (const c of [...checks]) {
|
|
1491
1649
|
if (c.pseudo || c.test == null) continue;
|
|
@@ -1600,7 +1758,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1600
1758
|
const r = rangeOfField(leaf.f);
|
|
1601
1759
|
const base = { needs: [], lo: r.hold.lo, hi: r.hold.hi };
|
|
1602
1760
|
if (!leaf.plain) return [base];
|
|
1603
|
-
const cv = numberOf(constValue([leaf.tok]));
|
|
1761
|
+
const cv = leaf.tok ? numberOf(constValue([leaf.tok])) : null;
|
|
1604
1762
|
if (cv != null && Number.isInteger(cv)) return [{ needs: [], lo: BigInt(cv), hi: BigInt(cv) }];
|
|
1605
1763
|
if (!heldBits) return [base];
|
|
1606
1764
|
let lo = null;
|
|
@@ -1669,9 +1827,19 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1669
1827
|
}
|
|
1670
1828
|
return out;
|
|
1671
1829
|
};
|
|
1830
|
+
// The derived intervals that hold every whole number from lo to hi, an open end holding nothing.
|
|
1831
|
+
const containing = (key, lo, hi) => {
|
|
1832
|
+
const out = [];
|
|
1833
|
+
for (const c of factsByKey.get(key) || []) {
|
|
1834
|
+
if (!c.derived) continue;
|
|
1835
|
+
const e = wholeEnds(c.tCons);
|
|
1836
|
+
if (e && (e.lo == null || (lo != null && e.lo <= lo)) && (e.hi == null || (hi != null && e.hi >= hi))) out.push(c.t);
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
};
|
|
1672
1840
|
const expandRules = () => {
|
|
1673
1841
|
indexFacts();
|
|
1674
|
-
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; }
|
|
1842
|
+
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; n.genArithOn = undefined; }
|
|
1675
1843
|
for (const r of rules) {
|
|
1676
1844
|
const bits = withLooser(r.fact);
|
|
1677
1845
|
const at = r.bucket == null ? r.n : nodes[r.n.test];
|
|
@@ -1680,6 +1848,26 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1680
1848
|
else ((at.genArithOn ||= { true: [], false: [] })[r.bucket]).push(...bits);
|
|
1681
1849
|
} else (r.n.derive ||= []).push({ needs: r.needs, to: bits });
|
|
1682
1850
|
}
|
|
1851
|
+
// A constant moved, and a check's outcome, are values too: routes that reached an interval by
|
|
1852
|
+
// arithmetic and routes that reached it by a move or a test meet at it.
|
|
1853
|
+
for (const a of assigns) {
|
|
1854
|
+
const v = numberOf(a.value);
|
|
1855
|
+
if (v == null || !Number.isInteger(v)) continue;
|
|
1856
|
+
const bits = containing(fieldKey(a.field), BigInt(v), BigInt(v));
|
|
1857
|
+
if (bits.length) (a.n.genArith ||= []).push(...bits);
|
|
1858
|
+
}
|
|
1859
|
+
for (const id of tests) {
|
|
1860
|
+
const n = nodes[id];
|
|
1861
|
+
for (const c of n.checks || []) {
|
|
1862
|
+
for (const [cons, bucket] of [[c.tCons, 'true'], [c.fCons, 'false']]) {
|
|
1863
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
1864
|
+
const e = wholeEnds(cons);
|
|
1865
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
1866
|
+
const bits = containing(c.key, e.lo, e.hi);
|
|
1867
|
+
if (bits.length) ((n.genArithOn ||= { true: [], false: [] })[bucket]).push(...bits);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1683
1871
|
};
|
|
1684
1872
|
|
|
1685
1873
|
const ARITH_VERBS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'MOVE']);
|
|
@@ -1873,6 +2061,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1873
2061
|
const before = relevant.size;
|
|
1874
2062
|
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);
|
|
1875
2063
|
for (const r of relations.values()) if (relevant.has(r.f) || relevant.has(r.g)) { relevant.add(r.f); relevant.add(r.g); }
|
|
2064
|
+
// A loop condition that bounds a relevant counter through another field makes that field relevant.
|
|
2065
|
+
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); }
|
|
1876
2066
|
more = relevant.size > before;
|
|
1877
2067
|
}
|
|
1878
2068
|
|
|
@@ -1884,6 +2074,20 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1884
2074
|
let grew = false;
|
|
1885
2075
|
try {
|
|
1886
2076
|
indexFacts();
|
|
2077
|
+
// A check's outcome as an interval of its own, which arithmetic's intervals can widen into at a join.
|
|
2078
|
+
for (const id of tests) {
|
|
2079
|
+
for (const c of nodes[id].checks || []) {
|
|
2080
|
+
if (!relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2081
|
+
for (const cons of [c.tCons, c.fCons]) {
|
|
2082
|
+
if (!cons || (cons.set && cons.set.size)) continue;
|
|
2083
|
+
const e = wholeEnds(cons);
|
|
2084
|
+
if (!e || (e.lo == null && e.hi == null)) continue;
|
|
2085
|
+
const had = derivedOf.get(c.field)?.size || 0;
|
|
2086
|
+
derivedFact(c.field, e.lo, e.hi, nodes[id].at || nodes[id].st);
|
|
2087
|
+
if ((derivedOf.get(c.field)?.size || 0) > had) grew = true;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1887
2091
|
for (const a of arith) {
|
|
1888
2092
|
a.trees.forEach((tree, i) => {
|
|
1889
2093
|
const r = a.targets[i];
|
|
@@ -1920,6 +2124,27 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1920
2124
|
}
|
|
1921
2125
|
}
|
|
1922
2126
|
}
|
|
2127
|
+
// A bound over an expression in one other field, X <= G - 1, takes the interval G holds at the test.
|
|
2128
|
+
for (const c of checks) {
|
|
2129
|
+
if (c.pseudo || c.test == null || !relevant.has(c.field) || !rangeOfField(c.field) || inTable(c.field)) continue;
|
|
2130
|
+
for (const o of [true, false]) {
|
|
2131
|
+
const dep = o ? c.tDep : c.fDep;
|
|
2132
|
+
const at = dep && outcomeAt.get(`${c.test}|${o}`);
|
|
2133
|
+
if (!at || !rangeOfField(dep.g) || inTable(dep.g) || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) continue;
|
|
2134
|
+
const hold = rangeOfField(c.field).hold;
|
|
2135
|
+
heldBits = prev ? prev.get(c.test) || null : null;
|
|
2136
|
+
for (const cand of leafCands({ f: dep.g, tok: null, plain: true })) {
|
|
2137
|
+
const v = BigInt(dep.m) * (dep.needs === 'hi' ? cand.hi : cand.lo) + BigInt(dep.b);
|
|
2138
|
+
let lo = null, hi = null;
|
|
2139
|
+
if (dep.side === 'hi') hi = dep.inc ? v : v - 1n; else lo = dep.inc ? v : v + 1n;
|
|
2140
|
+
if (lo != null && lo <= hold.lo) lo = null;
|
|
2141
|
+
if (hi != null && hi >= hold.hi) hi = null;
|
|
2142
|
+
if ((lo == null && hi == null) || (hi != null && hi < hold.lo) || (lo != null && lo > hold.hi)) continue;
|
|
2143
|
+
const fact = derivedFact(c.field, lo, hi, at.st || nodes[c.test].at || nodes[c.test].st);
|
|
2144
|
+
if (fact && derive(at, cand.needs, fact, String(o))) grew = true;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
1923
2148
|
// A relation holds after the outcome of its test that makes it true, and the fields it bounds hold
|
|
1924
2149
|
// what the other side could hold.
|
|
1925
2150
|
for (const id of tests) {
|
|
@@ -2049,6 +2274,36 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2049
2274
|
} else if (n.kind === 'step' && stepped.has(id)) {
|
|
2050
2275
|
keepAcross(n, [{ ...stepped.get(id), sizeError: false }]);
|
|
2051
2276
|
}
|
|
2277
|
+
// An upper bound survives a decrement that cannot go below 0: one where the field was at least the
|
|
2278
|
+
// amount before it, so nothing is stored as an absolute value or wraps.
|
|
2279
|
+
if (n.kind === 'stmt' && n.st && n.st.at != null) {
|
|
2280
|
+
const dec = decrements(n.st);
|
|
2281
|
+
const wrote = dec ? writtenBy(n) : [];
|
|
2282
|
+
for (const field of dec ? dec.fields : []) {
|
|
2283
|
+
if (wrote.some((w) => w !== field && overlaps(w, field))) continue;
|
|
2284
|
+
const key = fieldKey(field);
|
|
2285
|
+
const keep = [];
|
|
2286
|
+
const guards = [];
|
|
2287
|
+
for (const c of checks) {
|
|
2288
|
+
if (c.key !== key || c.pseudo) continue;
|
|
2289
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2290
|
+
if (bit == null || !cons || cons.dep || (cons.set && cons.set.size)) continue;
|
|
2291
|
+
const r = asRange(cons);
|
|
2292
|
+
if (r.hi != null && r.lo == null) keep.push(bit);
|
|
2293
|
+
if (r.lo != null && (r.loInc ? r.lo : Math.floor(r.lo) + 1) >= dec.max) guards.push(bit);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (!keep.length || !guards.length) continue;
|
|
2297
|
+
const bits = new Uint32Array(words);
|
|
2298
|
+
for (const b of keep) bits[b >>> 5] |= 1 << (b & 31);
|
|
2299
|
+
(n.keeps ||= []).push({ bits, guards });
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (n.kind === 'step' && keptOnStep.has(id)) {
|
|
2303
|
+
const bits = new Uint32Array(words);
|
|
2304
|
+
for (const b of keptOnStep.get(id)) bits[b >>> 5] |= 1 << (b & 31);
|
|
2305
|
+
(n.keeps ||= []).push({ bits, guards: null });
|
|
2306
|
+
}
|
|
2052
2307
|
});
|
|
2053
2308
|
|
|
2054
2309
|
// ---- must-analysis ----
|
|
@@ -2323,6 +2578,39 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2323
2578
|
const posOf = new Map();
|
|
2324
2579
|
const places = [];
|
|
2325
2580
|
let placeFacts = nFacts;
|
|
2581
|
+
// The tightest end of a field's value the facts `bits` hold, or its picture's.
|
|
2582
|
+
const boundAt = (g, needs, bits) => {
|
|
2583
|
+
const hold = rangeOfField(g)?.hold;
|
|
2584
|
+
let best = hold ? Number(needs === 'hi' ? hold.hi : hold.lo) : null;
|
|
2585
|
+
if (best != null && !Number.isFinite(best)) best = null;
|
|
2586
|
+
for (const c of checks) {
|
|
2587
|
+
if (c.pseudo || c.key !== fieldKey(g)) continue;
|
|
2588
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2589
|
+
if (!cons || !hasFact(bits, bit)) continue;
|
|
2590
|
+
const e = wholeEnds(cons);
|
|
2591
|
+
const v = e && (needs === 'hi' ? e.hi : e.lo);
|
|
2592
|
+
if (v == null) continue;
|
|
2593
|
+
const n = Number(v);
|
|
2594
|
+
if (best == null || (needs === 'hi' ? n < best : n > best)) best = n;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
return best;
|
|
2598
|
+
};
|
|
2599
|
+
// A relation over an expression, X <= G - 1, read with what G held at the test.
|
|
2600
|
+
const resolveDeps = (map, bits) => {
|
|
2601
|
+
const out = new Map();
|
|
2602
|
+
for (const [key, c] of map) {
|
|
2603
|
+
const dep = c && c.dep;
|
|
2604
|
+
const g = dep ? boundAt(dep.g, dep.needs, bits) : null;
|
|
2605
|
+
if (g == null || !Number.isInteger(dep.m) || !Number.isInteger(dep.b)) { out.set(key, c); continue; }
|
|
2606
|
+
const v = dep.m * g + dep.b;
|
|
2607
|
+
const plain = plainCons(c) || {};
|
|
2608
|
+
const had = plain[dep.side];
|
|
2609
|
+
const tighter = had == null || (dep.side === 'hi' ? v < had : v > had);
|
|
2610
|
+
out.set(key, tighter ? { ...plain, [dep.side]: v, [`${dep.side}Inc`]: dep.inc } : plain);
|
|
2611
|
+
}
|
|
2612
|
+
return out;
|
|
2613
|
+
};
|
|
2326
2614
|
for (const id of tests) {
|
|
2327
2615
|
const n = nodes[id];
|
|
2328
2616
|
if (!n.st || !n.st.indexes || !n.tree || n.evaluate || !(n.st.verb === 'IF' || n.loop)) continue;
|
|
@@ -2337,7 +2625,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2337
2625
|
for (const g of guards || []) {
|
|
2338
2626
|
let cons = new Map();
|
|
2339
2627
|
const r = new Map();
|
|
2340
|
-
|
|
2628
|
+
const atTest = facts.get(id);
|
|
2629
|
+
for (const [sub, outcome] of g) { cons = meetMaps(cons, atTest ? resolveDeps(consOf(sub, outcome), atTest) : consOf(sub, outcome)); testedWhen(sub, outcome, r); }
|
|
2341
2630
|
said = said ? joinMaps(said, cons) : cons;
|
|
2342
2631
|
ran = ran ? new Map([...ran].filter(([k]) => r.has(k))) : r;
|
|
2343
2632
|
}
|