@portll/cobolwork 0.2.76 → 0.2.140
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 +66 -14
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1169 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +276 -126
- package/lib/embedded-sql.mjs +110 -0
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +94 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +9 -5
- package/lib/exploitability.mjs +263 -0
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +109 -0
- package/lib/jcl.mjs +27 -7
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/layout.mjs +202 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +80 -209
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +5 -84
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +235 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +119 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +70 -106
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +246 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +229 -15
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- package/schema/cobolwork.policy.schema.json +94 -16
package/lib/control.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// entered holding no facts at all, which can only withhold credit. ALTER makes a GO TO reach every
|
|
21
21
|
// paragraph, for the same reason.
|
|
22
22
|
import { segmentEnd } from './parser.mjs';
|
|
23
|
+
import { parser, attempt, candidates, leaves, storedRange } from './arith.mjs';
|
|
23
24
|
|
|
24
25
|
const WORD = (t, u) => t && t.t === 'word' && t.u === u;
|
|
25
26
|
const PHRASED = new Set(['READ', 'WRITE', 'REWRITE', 'DELETE', 'START', 'RETURN', 'CALL', 'COMPUTE', 'ADD', 'SUBTRACT',
|
|
@@ -28,6 +29,8 @@ const PHRASE_WORDS = new Set(['END', 'INVALID', 'EXCEPTION', 'OVERFLOW', 'END-OF
|
|
|
28
29
|
// One copy of every node's facts. The analysis keeps a few, so this is a quarter of what it may use.
|
|
29
30
|
const FACT_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
30
31
|
const SETTLE_BUDGET = 20 * 1000 * 1000;
|
|
32
|
+
// Statements times fact words past which arithmetic intervals are not iterated.
|
|
33
|
+
const ARITH_WORK = 40 * 1000 * 1000;
|
|
31
34
|
// Routines whose only effect is to end the run with an abend.
|
|
32
35
|
const ABEND_ROUTINE = /^(CEE3ABD|CEE3AB2|ILBOABN0|ILBOABN)$/i;
|
|
33
36
|
|
|
@@ -50,6 +53,8 @@ function meet(a, b) {
|
|
|
50
53
|
if (a.set && b.set) out.set = new Set([...a.set].filter((v) => b.set.has(v)));
|
|
51
54
|
else if (a.set || b.set) out.set = new Set(a.set || b.set);
|
|
52
55
|
if (a.numeric || b.numeric) out.numeric = true;
|
|
56
|
+
if (!a.dep !== !b.dep) out.dep = a.dep || b.dep;
|
|
57
|
+
if (a.viaExpr && b.viaExpr) out.viaExpr = true;
|
|
53
58
|
if (a.alpha || b.alpha) out.alpha = true;
|
|
54
59
|
const lo = pick(a, b, 'lo', Math.max);
|
|
55
60
|
const hi = pick(a, b, 'hi', Math.min);
|
|
@@ -67,9 +72,10 @@ function join(a, b) {
|
|
|
67
72
|
if (a.set && b.set) out.set = new Set([...a.set, ...b.set]);
|
|
68
73
|
if (a.numeric && b.numeric) out.numeric = true;
|
|
69
74
|
if (a.alpha && b.alpha) out.alpha = true;
|
|
75
|
+
if (a.viaExpr || b.viaExpr) out.viaExpr = true;
|
|
70
76
|
if (a.lo != null && b.lo != null) { out.lo = Math.min(a.lo, b.lo); out.loInc = a.lo === out.lo ? a.loInc : b.loInc; }
|
|
71
77
|
if (a.hi != null && b.hi != null) { out.hi = Math.max(a.hi, b.hi); out.hiInc = a.hi === out.hi ? a.hiInc : b.hiInc; }
|
|
72
|
-
return Object.keys(out).
|
|
78
|
+
return Object.keys(out).some((k) => k !== 'viaExpr') ? out : null;
|
|
73
79
|
}
|
|
74
80
|
// Whether a known value meets what a check's outcome says of its field.
|
|
75
81
|
function satisfies(cons, v) {
|
|
@@ -99,6 +105,9 @@ function pick(a, b, k, better) {
|
|
|
99
105
|
return v === av.v ? av : bv;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
const INDEX_SINKS = new Set(['subscript', 'reference-modification', 'occurs-depending-count', 'loop-bound']);
|
|
109
|
+
const atLeastOne = (cons) => cons.lo != null && (cons.lo >= 1 || (cons.lo >= 0 && !cons.loInc));
|
|
110
|
+
|
|
102
111
|
// Whether a constraint makes a value safe for what the sink does with it. A value that can only be
|
|
103
112
|
// one of a set of literals is safe everywhere. Digits alone cannot carry a command, a statement, a
|
|
104
113
|
// script or a job, and are exactly what arithmetic needs. Letters and spaces cannot carry markup or a
|
|
@@ -109,12 +118,16 @@ export function stops(cons, sinkKind) {
|
|
|
109
118
|
const allDigits = set && [...cons.set].every((v) => /^[+-]?\d+(\.\d+)?$/.test(v));
|
|
110
119
|
switch (sinkKind) {
|
|
111
120
|
case 'arithmetic': return cons.numeric === true || allDigits;
|
|
112
|
-
|
|
121
|
+
// A subscript and a reference-modification start count from 1, so 0 is out of range too; a
|
|
122
|
+
// count or a loop bound of 0 is not.
|
|
123
|
+
case 'subscript': case 'reference-modification':
|
|
124
|
+
return (cons.hi != null && atLeastOne(cons)) || (allDigits && [...cons.set].every((v) => Number(v) >= 1));
|
|
125
|
+
case 'occurs-depending-count': case 'loop-bound': case 'storage-length':
|
|
113
126
|
return cons.hi != null || allDigits;
|
|
114
127
|
case 'dynamic-sql': case 'internal-reader': return set || cons.numeric === true;
|
|
115
128
|
case 'web-response': case 'http-header': case 'log': return set || cons.numeric === true || cons.alpha === true;
|
|
116
129
|
case 'os-command': case 'dynamic-program-load': case 'cics-dynamic-transfer': case 'dynamic-file-path':
|
|
117
|
-
case 'queue-name': case 'outbound-host': return set;
|
|
130
|
+
case 'queue-name': case 'outbound-host': case 'connection-target': case 'cics-sysid': case 'cics-system-resource': return set;
|
|
118
131
|
default: return false;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
@@ -127,6 +140,8 @@ const SIGN = new Set(['POSITIVE', 'NEGATIVE', 'ZERO', 'ZEROS', 'ZEROES']);
|
|
|
127
140
|
const FIGURATIVE = { SPACE: ' ', SPACES: ' ', ZERO: '0', ZEROS: '0', ZEROES: '0', 'LOW-VALUE': '\u0000', 'LOW-VALUES': '\u0000', 'HIGH-VALUE': 'ÿ', 'HIGH-VALUES': 'ÿ', QUOTE: '"', QUOTES: '"' };
|
|
128
141
|
const NEGATE = { '=': '<>', '<>': '=', '>': '<=', '<=': '>', '<': '>=', '>=': '<' };
|
|
129
142
|
const FLIP = { '=': '=', '<>': '<>', '>': '<', '<': '>', '>=': '<=', '<=': '>=' };
|
|
143
|
+
// Past this an expression's intermediate value may not be held exactly, and no bound is drawn from it.
|
|
144
|
+
const EXPR_LIMIT = 1e15;
|
|
130
145
|
const RESTRICTING_OPS = new Set(['>', '<', '>=', '<=']);
|
|
131
146
|
|
|
132
147
|
// A condition as a tree of AND, OR and NOT over atoms. It reads what COBOL writes - class and sign
|
|
@@ -233,6 +248,7 @@ export function parseCondition(toks, resolve) {
|
|
|
233
248
|
export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
234
249
|
if (!prog.proc) return null;
|
|
235
250
|
const { tokens, from, to } = prog.proc;
|
|
251
|
+
const loopBody = new Map();
|
|
236
252
|
const stmtAt = new Map();
|
|
237
253
|
for (const st of prog.statements) if (st.at != null && st.verb !== 'WHEN') stmtAt.set(st.at, st);
|
|
238
254
|
const labelAt = new Map();
|
|
@@ -357,6 +373,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
357
373
|
// A name that is neither a paragraph nor data nor a loop word is a paragraph the parse missed.
|
|
358
374
|
if (first && first.t === 'word' && !stmtAt.has(st.at + 1) && !LOOP_START.has(first.u) && !/^\d+$/.test(first.v) && !resolve(first)) unknownLabel = true;
|
|
359
375
|
const body = parseList(new Set([...stops, 'END-PERFORM']));
|
|
376
|
+
// Where an inline body lies in the tokens, and whether it can run code outside them.
|
|
377
|
+
let leaves = false;
|
|
378
|
+
for (let j = st.end; j < k; j++) if (isWord(j, 'GO') || (isWord(j, 'PERFORM') && tokens[j + 1] && tokens[j + 1].t === 'word' && labelNames.has(tokens[j + 1].u))) leaves = true;
|
|
379
|
+
loopBody.set(st, { from: st.at, to: k, leaves });
|
|
360
380
|
if (isWord(k, 'END-PERFORM')) k++;
|
|
361
381
|
return { k: 'inline', st, body, loop, testAfter };
|
|
362
382
|
}
|
|
@@ -469,6 +489,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
469
489
|
const sectionEnd = new Map();
|
|
470
490
|
for (const p of paras) if (p.section) sectionEnd.set(p.section, p.end);
|
|
471
491
|
const tests = [];
|
|
492
|
+
const loopHeads = [];
|
|
472
493
|
|
|
473
494
|
function lowerList(list, next, ctx) {
|
|
474
495
|
let cont = next;
|
|
@@ -553,6 +574,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
553
574
|
const body = n.k === 'perform' ? add('call', { st: n.st, range: rangeOf(n.target, n.thru), succ: [step] }) : lowerList(n.body, step, loopCtx);
|
|
554
575
|
nodes[t].succ = [outcome(t, true, next), outcome(t, false, body)];
|
|
555
576
|
nodes[head].succ = [n.testAfter ? body : t];
|
|
577
|
+
if (n.st.loops) loopHeads.push({ head, step, loops: n.st.loops, test: t, testAfter: n.testAfter, body: n.k === 'inline' ? n.body : null });
|
|
556
578
|
return head;
|
|
557
579
|
}
|
|
558
580
|
case 'goto': {
|
|
@@ -601,6 +623,64 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
601
623
|
return top;
|
|
602
624
|
};
|
|
603
625
|
const fieldKey = (f) => (f.index ? `IX:${f.index}` : f);
|
|
626
|
+
const overlaps = (w, f) => {
|
|
627
|
+
if (w === f) return true;
|
|
628
|
+
if (w.index || f.index) return w.index && f.index && w.index === f.index;
|
|
629
|
+
if (w.level === 66 || f.level === 66) return true;
|
|
630
|
+
if (itemsInRecord(w) !== itemsInRecord(f)) return false;
|
|
631
|
+
if (w.offset == null || f.offset == null || w.size == null || f.size == null) return true;
|
|
632
|
+
const wEnd = w.offset + (w.contributes || w.size);
|
|
633
|
+
const fEnd = f.offset + (f.contributes || f.size);
|
|
634
|
+
return w.offset < fEnd && f.offset < wEnd;
|
|
635
|
+
};
|
|
636
|
+
const writtenBy = (n) => {
|
|
637
|
+
const out = [];
|
|
638
|
+
const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
|
|
639
|
+
if (n.st) {
|
|
640
|
+
for (const t of n.st.targets || []) add(t);
|
|
641
|
+
if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
|
|
642
|
+
// A callee may write what it is passed by reference, and a READ fills the file's records.
|
|
643
|
+
for (const t of n.st.sources || []) {
|
|
644
|
+
const f = resolve(t);
|
|
645
|
+
if (!f) continue;
|
|
646
|
+
if (f.records) for (const r of f.records) out.push(r);
|
|
647
|
+
else if (n.st.verb === 'CALL') out.push(f);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (n.e && n.e.kind === 'SQL') {
|
|
652
|
+
// A statement fills the host variables of its INTO list and reads the rest.
|
|
653
|
+
const toks = n.e.toks;
|
|
654
|
+
const into = toks.findIndex((t) => WORD(t, 'INTO'));
|
|
655
|
+
if (into >= 0) {
|
|
656
|
+
for (let i = into + 1; i < toks.length && !WORD(toks[i], 'FROM'); i++) {
|
|
657
|
+
if (toks[i].t === 'op' && toks[i].v === ':' && toks[i + 1]) add(toks[i + 1]);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} else if (n.e) {
|
|
661
|
+
// A command may fill what any option is given directly, except what it sends FROM. A name
|
|
662
|
+
// inside that argument's own parentheses - a subscript, a reference modification - is read.
|
|
663
|
+
let depth = 0;
|
|
664
|
+
let option = null;
|
|
665
|
+
for (let i = 0; i < n.e.toks.length; i++) {
|
|
666
|
+
const t = n.e.toks[i];
|
|
667
|
+
if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
|
|
668
|
+
if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
|
|
669
|
+
if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return out;
|
|
673
|
+
};
|
|
674
|
+
// A VALUE holds for the whole run only if no statement writes storage that overlaps the item.
|
|
675
|
+
let writtenItems = null;
|
|
676
|
+
const neverWritten = (it) => {
|
|
677
|
+
if (it.external) return false;
|
|
678
|
+
if (!writtenItems) {
|
|
679
|
+
writtenItems = [];
|
|
680
|
+
for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call') writtenItems.push(...writtenBy(n));
|
|
681
|
+
}
|
|
682
|
+
return !writtenItems.some((w) => overlaps(w, it));
|
|
683
|
+
};
|
|
604
684
|
const constValue = (toks) => {
|
|
605
685
|
if (toks.length === 3 && WORD(toks[0], 'LENGTH') && WORD(toks[1], 'OF')) {
|
|
606
686
|
const it = resolve(toks[2]);
|
|
@@ -613,7 +693,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
613
693
|
if (t.t === 'word' && FIGURATIVE[t.u] != null) return FIGURATIVE[t.u];
|
|
614
694
|
const it = resolve(t);
|
|
615
695
|
// A level-78 constant, or a field with a VALUE that no statement writes, stands for its value.
|
|
616
|
-
if (it && !it.index && (it.level === 78 || it.constant || (!it.receiving && it.values && it.values.length === 1))) {
|
|
696
|
+
if (it && !it.index && (it.level === 78 || it.constant || (!it.receiving && it.values && it.values.length === 1 && neverWritten(it)))) {
|
|
617
697
|
const v = it.values[0];
|
|
618
698
|
if (v && v.t === 'lit') return String(v.v).trimEnd();
|
|
619
699
|
if (v && (v.t === 'num' || /^[+-]?\d+(\.\d+)?$/.test(v.v))) return String(Number(v.v));
|
|
@@ -660,6 +740,157 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
660
740
|
if (range) return join(range, { set: new Set(vals) }) || null;
|
|
661
741
|
return vals.length ? { set: new Set(vals) } : null;
|
|
662
742
|
};
|
|
743
|
+
const pictureOf = (field) => String(field.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n)));
|
|
744
|
+
const categories = new Map();
|
|
745
|
+
const categoryOf = (field) => {
|
|
746
|
+
if (categories.has(field)) return categories.get(field);
|
|
747
|
+
const usage = String(field.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
|
|
748
|
+
const pic = pictureOf(field);
|
|
749
|
+
const cat = (field.children || []).some((c) => c.level !== 88) ? { k: 'A' }
|
|
750
|
+
: /^[XA]+$/.test(pic) && usage === 'DISPLAY' ? { k: 'A' }
|
|
751
|
+
: /^S?9+(V9*)?$/.test(pic) && /^(DISPLAY|COMP|COMP-3|COMP-4|COMP-5|BINARY|PACKED-DECIMAL)$/.test(usage) ? { k: 'N', digits: pic.split('V')[0].replace(/[^9]/g, '').length }
|
|
752
|
+
: null;
|
|
753
|
+
categories.set(field, cat);
|
|
754
|
+
return cat;
|
|
755
|
+
};
|
|
756
|
+
// An unsigned whole number, for which "not zero" is "at least one".
|
|
757
|
+
const unsignedInteger = (field) => !!field && !field.index && categoryOf(field)?.k === 'N' && /^9+$/.test(pictureOf(field));
|
|
758
|
+
// What a numeric item's picture and storage allow: an unsigned item is not negative, a signed one
|
|
759
|
+
// has no known lower end, and a binary item may fill its bytes. Null for anything else.
|
|
760
|
+
const picRange = (f) => {
|
|
761
|
+
if (!f || f.index || f.level === 88 || f.level === 66 || categoryOf(f)?.k !== 'N') return null;
|
|
762
|
+
const pic = pictureOf(f);
|
|
763
|
+
let hi = 10 ** pic.split('V')[0].replace(/[^9]/g, '').length - (pic.includes('V') ? 0 : 1);
|
|
764
|
+
if (/^(COMP|COMP-4|COMP-5|BINARY)$/.test(String(f.effectiveUsage || '').replace('COMPUTATIONAL', 'COMP')) && f.size > 0) hi = Math.max(hi, 2 ** (8 * f.size) - 1);
|
|
765
|
+
return { lo: pic.startsWith('S') ? -Infinity : 0, hi };
|
|
766
|
+
};
|
|
767
|
+
const variableLength = (it) => !!it.dependingOn || (it.children || []).some(variableLength);
|
|
768
|
+
const linAdd = (a, b, sign) => {
|
|
769
|
+
if (!a || !b) return null;
|
|
770
|
+
const terms = new Map(a.terms);
|
|
771
|
+
for (const [f, c] of b.terms) { const n = (terms.get(f) || 0) + sign * c; if (n) terms.set(f, n); else terms.delete(f); }
|
|
772
|
+
const r = { terms, lo: a.lo + (sign > 0 ? b.lo : -b.hi), hi: a.hi + (sign > 0 ? b.hi : -b.lo) };
|
|
773
|
+
return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
|
|
774
|
+
};
|
|
775
|
+
// An arithmetic expression as `terms` (a coefficient per numeric field) plus a `lo`..`hi` interval
|
|
776
|
+
// for its constants, or null for what it cannot read exactly: division, a power, a product of two
|
|
777
|
+
// unknowns, a subscript, a non-numeric name, or a number past EXPR_LIMIT where the compiler's
|
|
778
|
+
// intermediate precision could give way.
|
|
779
|
+
const linearOf = (toks) => {
|
|
780
|
+
let i = 0;
|
|
781
|
+
const isOp = (v) => toks[i] && toks[i].t === 'op' && toks[i].v === v;
|
|
782
|
+
const isSep = (v) => toks[i] && toks[i].t === 'sep' && toks[i].v === v;
|
|
783
|
+
const constant = (n) => (Math.abs(n) > EXPR_LIMIT ? null : { terms: new Map(), lo: n, hi: n });
|
|
784
|
+
// LENGTH OF is the item's size, which a table that varies in length only bounds.
|
|
785
|
+
const sized = (tok) => {
|
|
786
|
+
const it = tok && tok.t === 'word' ? resolve(tok) : null;
|
|
787
|
+
if (!it || it.index || it.level === 88 || it.size == null || it.size > EXPR_LIMIT) return null;
|
|
788
|
+
return { terms: new Map(), lo: variableLength(it) ? 0 : it.size, hi: it.size };
|
|
789
|
+
};
|
|
790
|
+
const times = (a, b) => {
|
|
791
|
+
if (!a || !b) return null;
|
|
792
|
+
const scalar = (e) => (!e.terms.size && e.lo === e.hi ? e.lo : null);
|
|
793
|
+
const k = scalar(a) ?? scalar(b);
|
|
794
|
+
const e = scalar(a) != null ? b : a;
|
|
795
|
+
if (k == null && (a.terms.size || b.terms.size)) return null;
|
|
796
|
+
if (k == null) {
|
|
797
|
+
const ends = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
|
|
798
|
+
const r = { terms: new Map(), lo: Math.min(...ends), hi: Math.max(...ends) };
|
|
799
|
+
return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
|
|
800
|
+
}
|
|
801
|
+
const terms = new Map();
|
|
802
|
+
for (const [f, c] of e.terms) if (c * k) terms.set(f, c * k);
|
|
803
|
+
const ends = [e.lo * k, e.hi * k];
|
|
804
|
+
const r = { terms, lo: Math.min(...ends), hi: Math.max(...ends) };
|
|
805
|
+
return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
|
|
806
|
+
};
|
|
807
|
+
const primary = () => {
|
|
808
|
+
const t = toks[i];
|
|
809
|
+
if (!t) return null;
|
|
810
|
+
if (isSep('(')) {
|
|
811
|
+
i++;
|
|
812
|
+
const e = sum();
|
|
813
|
+
if (!isSep(')')) return null;
|
|
814
|
+
i++;
|
|
815
|
+
return e;
|
|
816
|
+
}
|
|
817
|
+
if (t.t !== 'word' && t.t !== 'num') return null;
|
|
818
|
+
if (WORD(t, 'LENGTH') && WORD(toks[i + 1], 'OF')) { const e = sized(toks[i + 2]); i += 3; return e; }
|
|
819
|
+
if (WORD(t, 'FUNCTION') && WORD(toks[i + 1], 'LENGTH') && toks[i + 2] && toks[i + 2].v === '(') {
|
|
820
|
+
const e = sized(toks[i + 3]);
|
|
821
|
+
if (!toks[i + 4] || toks[i + 4].v !== ')') return null;
|
|
822
|
+
i += 5;
|
|
823
|
+
return e;
|
|
824
|
+
}
|
|
825
|
+
i++;
|
|
826
|
+
while ((WORD(toks[i], 'OF') || WORD(toks[i], 'IN')) && toks[i + 1] && toks[i + 1].t === 'word') i += 2;
|
|
827
|
+
if (isSep('(')) return null;
|
|
828
|
+
const n = numberOf(constValue([t]));
|
|
829
|
+
if (n != null) return constant(n);
|
|
830
|
+
const f = t.t === 'word' ? resolve(t) : null;
|
|
831
|
+
return picRange(f) ? { terms: new Map([[f, 1]]), lo: 0, hi: 0 } : null;
|
|
832
|
+
};
|
|
833
|
+
const factor = () => {
|
|
834
|
+
if (isOp('-')) { i++; return times(constant(-1), primary()); }
|
|
835
|
+
if (isOp('+')) i++;
|
|
836
|
+
return primary();
|
|
837
|
+
};
|
|
838
|
+
const product = () => {
|
|
839
|
+
let e = factor();
|
|
840
|
+
while (e && isOp('*')) { i++; e = times(e, factor()); }
|
|
841
|
+
return e;
|
|
842
|
+
};
|
|
843
|
+
function sum() {
|
|
844
|
+
let e = product();
|
|
845
|
+
while (e && (isOp('+') || isOp('-'))) { const sign = toks[i].v === '+' ? 1 : -1; i++; e = linAdd(e, product(), sign); }
|
|
846
|
+
return e;
|
|
847
|
+
}
|
|
848
|
+
const e = sum();
|
|
849
|
+
return e && i === toks.length ? e : null;
|
|
850
|
+
};
|
|
851
|
+
// What `left op right` says of each field in it that has coefficient 1 or -1, from the rest of the
|
|
852
|
+
// expression: X + Y <= K gives X <= K - Y, and Y unsigned gives X <= K. The other operand counts by
|
|
853
|
+
// what its picture rules out (a sign) and by constants alone; a bound it has only from checks is
|
|
854
|
+
// left as `dep`, for the second pass to join to those checks. Only what narrows the field is kept.
|
|
855
|
+
const exprCons = (left, op, right) => {
|
|
856
|
+
const l = linearOf(left), r = linearOf(right);
|
|
857
|
+
const e = l && r ? linAdd(l, r, -1) : null;
|
|
858
|
+
const out = new Map();
|
|
859
|
+
if (!e || !['<', '<=', '>', '>='].includes(op)) return out;
|
|
860
|
+
const upper = op === '<' || op === '<=';
|
|
861
|
+
const inc = op === '<=' || op === '>=';
|
|
862
|
+
for (const [x, c] of e.terms) {
|
|
863
|
+
if (Math.abs(c) !== 1) continue;
|
|
864
|
+
const own = picRange(x);
|
|
865
|
+
// c*x + sum(d*g) + R op 0, R in [e.lo, e.hi]: x is on the `side` of the sum of m*g, plus b.
|
|
866
|
+
const side = upper === (c > 0) ? 'hi' : 'lo';
|
|
867
|
+
const b = -c * (upper ? e.lo : e.hi);
|
|
868
|
+
const open = [];
|
|
869
|
+
const soft = [];
|
|
870
|
+
for (const [g, d] of e.terms) {
|
|
871
|
+
if (g === x) continue;
|
|
872
|
+
const m = -c * d;
|
|
873
|
+
const needs = side === 'hi' ? (m > 0 ? 'hi' : 'lo') : (m > 0 ? 'lo' : 'hi');
|
|
874
|
+
if (!(needs === 'lo' && picRange(g).lo === 0)) open.push({ g, m, needs }); else soft.push({ g, m, needs });
|
|
875
|
+
}
|
|
876
|
+
if (open.length > 1) continue;
|
|
877
|
+
const c1 = { viaExpr: true };
|
|
878
|
+
// An operand whose picture gives 0 for its low end may have a higher one from a check: the picture's bound stands, and the check's tightens it.
|
|
879
|
+
if (!open.length) {
|
|
880
|
+
c1[side] = b; c1[`${side}Inc`] = inc;
|
|
881
|
+
if (soft.length === 1) c1.dep = { g: soft[0].g, m: soft[0].m, needs: soft[0].needs, side, b, inc };
|
|
882
|
+
} else c1.dep = { g: open[0].g, m: open[0].m, needs: open[0].needs, side, b, inc };
|
|
883
|
+
const narrows = c1[side] != null && (side === 'hi' ? c1.hi < own.hi || (c1.hi === own.hi && !inc) : c1.lo > own.lo || (c1.lo === own.lo && !inc));
|
|
884
|
+
if (narrows || c1.dep) out.set(fieldKey(x), c1);
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
};
|
|
888
|
+
// A constraint without its `dep`, or null where nothing else is left.
|
|
889
|
+
const plainCons = (c) => {
|
|
890
|
+
if (!c || !c.dep) return c || null;
|
|
891
|
+
const { dep, ...rest } = c;
|
|
892
|
+
return Object.keys(rest).some((k) => k !== 'viaExpr') ? rest : null;
|
|
893
|
+
};
|
|
663
894
|
// What a condition says each field holds when it is `outcome`, and which fields it tests.
|
|
664
895
|
function consOf(tree, outcome) {
|
|
665
896
|
switch (tree.k) {
|
|
@@ -674,7 +905,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
674
905
|
case 'sign': {
|
|
675
906
|
const f = fieldOf(tree.subject);
|
|
676
907
|
if (!f) return new Map();
|
|
677
|
-
const c = tree.sign === 'ZERO' ? (outcome ? { set: new Set(['0']) } : null)
|
|
908
|
+
const c = tree.sign === 'ZERO' ? (outcome ? { set: new Set(['0']) } : unsignedInteger(f) ? { lo: 1, loInc: true } : null)
|
|
678
909
|
: tree.sign === 'POSITIVE' ? (outcome ? { lo: 0, loInc: false } : { hi: 0, hiInc: true })
|
|
679
910
|
: (outcome ? { hi: 0, hiInc: false } : { lo: 0, loInc: true });
|
|
680
911
|
return c ? new Map([[fieldKey(f), c]]) : new Map();
|
|
@@ -686,9 +917,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
686
917
|
if (!f || v == null) {
|
|
687
918
|
const g = fieldOf(tree.right);
|
|
688
919
|
const w = constValue(tree.left);
|
|
689
|
-
if (g && w != null) { f = g; v = w; op = FLIP[op]; } else return
|
|
920
|
+
if (g && w != null) { f = g; v = w; op = FLIP[op]; } else return exprCons(tree.left, outcome ? tree.op : NEGATE[tree.op], tree.right);
|
|
690
921
|
}
|
|
691
|
-
const
|
|
922
|
+
const eff = outcome ? op : NEGATE[op];
|
|
923
|
+
const c = eff === '<>' && numberOf(v) === 0 && unsignedInteger(f) ? { lo: 1, loInc: true } : relCons(eff, v);
|
|
692
924
|
return c ? new Map([[fieldKey(f), c]]) : new Map();
|
|
693
925
|
}
|
|
694
926
|
case 'cond': {
|
|
@@ -787,7 +1019,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
787
1019
|
for (const key of keys) {
|
|
788
1020
|
const field = restricts.get(key) || (typeof key === 'string' ? { index: key.slice(3) } : key);
|
|
789
1021
|
noteField(key, field);
|
|
790
|
-
const c = { id: checks.length, key, field, file: where.file, line: where.line, tCons: whenTrue.get(key) || null,
|
|
1022
|
+
const c = { id: checks.length, key, field, file: where.file, line: where.line, test: id, tCons: plainCons(whenTrue.get(key)), fCons: plainCons(whenFalse.get(key)), tDep: whenTrue.get(key)?.dep || null, fDep: whenFalse.get(key)?.dep || null, restricts: restricts.has(key) };
|
|
791
1023
|
checks.push(c);
|
|
792
1024
|
mine.push(c);
|
|
793
1025
|
}
|
|
@@ -835,6 +1067,25 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
835
1067
|
a.c = { id: checks.length, key, field: a.field, file: a.n.st.file, line: a.n.st.line, tCons: { set: new Set([a.value]) }, fCons: null, restricts: false, assigned: true };
|
|
836
1068
|
checks.push(a.c);
|
|
837
1069
|
}
|
|
1070
|
+
// One fact per distinct constant v moved into a whole number: "it holds a constant from 1 to v", set by every MOVE of one that small, so it survives a join only where every route moved one.
|
|
1071
|
+
const moved = new Map();
|
|
1072
|
+
for (const a of assigns) {
|
|
1073
|
+
const cat = a.field.level === 88 ? null : categoryOf(a.field);
|
|
1074
|
+
const v = numberOf(a.value);
|
|
1075
|
+
if (!cat || cat.k !== 'N' || v == null || !Number.isInteger(v) || v < 1 || String(v).length > cat.digits) continue;
|
|
1076
|
+
if (!moved.has(a.field)) moved.set(a.field, []);
|
|
1077
|
+
moved.get(a.field).push(a);
|
|
1078
|
+
}
|
|
1079
|
+
for (const [field, list] of moved) {
|
|
1080
|
+
const key = fieldKey(field);
|
|
1081
|
+
noteField(key, field);
|
|
1082
|
+
for (const hi of new Set(list.map((a) => Number(a.value)))) {
|
|
1083
|
+
const bounds = { lo: 1, hi };
|
|
1084
|
+
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
|
+
checks.push(c);
|
|
1086
|
+
for (const a of list) if (Number(a.value) <= hi) (a.upTo ||= []).push(c);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
838
1089
|
// Fact numbers: for each check, `x` that it ran (only a restricting one), `t` and `f` that it came
|
|
839
1090
|
// out true or false with something to say about its field.
|
|
840
1091
|
let nFacts = 0;
|
|
@@ -842,6 +1093,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
842
1093
|
c.x = c.restricts ? nFacts++ : null;
|
|
843
1094
|
c.t = c.tCons ? nFacts++ : null;
|
|
844
1095
|
c.f = c.fCons ? nFacts++ : null;
|
|
1096
|
+
c.rt = c.tDep ? nFacts++ : null;
|
|
1097
|
+
c.rf = c.fDep ? nFacts++ : null;
|
|
845
1098
|
}
|
|
846
1099
|
for (const id of tests) {
|
|
847
1100
|
const n = nodes[id];
|
|
@@ -849,6 +1102,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
849
1102
|
if (c.x != null) { n.gen.true.push(c.x); n.gen.false.push(c.x); }
|
|
850
1103
|
if (c.t != null) n.gen.true.push(c.t);
|
|
851
1104
|
if (c.f != null) n.gen.false.push(c.f);
|
|
1105
|
+
if (c.rt != null) n.gen.true.push(c.rt);
|
|
1106
|
+
if (c.rf != null) n.gen.false.push(c.rf);
|
|
852
1107
|
}
|
|
853
1108
|
}
|
|
854
1109
|
// A constant also makes true every outcome it satisfies of a check on the same field, so a length
|
|
@@ -865,6 +1120,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
865
1120
|
for (const a of assigns) {
|
|
866
1121
|
const gen = (a.n.genStmt ||= []);
|
|
867
1122
|
gen.push(a.c.t);
|
|
1123
|
+
for (const c of a.upTo || []) gen.push(c.t);
|
|
868
1124
|
for (const c of checksOf.get(a.c.key) || []) {
|
|
869
1125
|
if (c.t != null && satisfies(c.tCons, a.value)) gen.push(c.t);
|
|
870
1126
|
if (c.f != null && satisfies(c.fCons, a.value)) gen.push(c.f);
|
|
@@ -896,6 +1152,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
896
1152
|
return it && !it.index && it.level !== 88 && it.size != null ? it.size : null;
|
|
897
1153
|
};
|
|
898
1154
|
const writesRecordOf = (st, item) => (st.targets || []).some((t) => { const it = resolve(t); return it && !it.index && itemsInRecord(it) === itemsInRecord(item); });
|
|
1155
|
+
const tallies = [];
|
|
899
1156
|
const tallyIn = (seq) => {
|
|
900
1157
|
for (let j = 0; j < seq.length; j++) {
|
|
901
1158
|
const el = seq[j];
|
|
@@ -921,6 +1178,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
921
1178
|
const bounds = { lo: start, hi: start + size };
|
|
922
1179
|
const c = { id: checks.length, key, field: count, file: el.st.file, line: el.st.line, tCons: { numeric: true, lo: bounds.lo, loInc: true, hi: bounds.hi, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds };
|
|
923
1180
|
checks.push(c);
|
|
1181
|
+
tallies.push(c);
|
|
924
1182
|
(n.genStmt ||= []).push(c.t);
|
|
925
1183
|
}
|
|
926
1184
|
break;
|
|
@@ -950,19 +1208,6 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
950
1208
|
// A flag's values are compared only where nothing but the literal decides the comparison: an
|
|
951
1209
|
// alphanumeric literal against an alphanumeric item, which a value it is given must fill so that
|
|
952
1210
|
// JUSTIFIED cannot move it, and an integer against a numeric item with room for its digits.
|
|
953
|
-
const pictureOf = (field) => String(field.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n)));
|
|
954
|
-
const categories = new Map();
|
|
955
|
-
const categoryOf = (field) => {
|
|
956
|
-
if (categories.has(field)) return categories.get(field);
|
|
957
|
-
const usage = String(field.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
|
|
958
|
-
const pic = pictureOf(field);
|
|
959
|
-
const cat = (field.children || []).some((c) => c.level !== 88) ? { k: 'A' }
|
|
960
|
-
: /^[XA]+$/.test(pic) && usage === 'DISPLAY' ? { k: 'A' }
|
|
961
|
-
: /^S?9+(V9*)?$/.test(pic) && /^(DISPLAY|COMP|COMP-3|COMP-4|COMP-5|BINARY|PACKED-DECIMAL)$/.test(usage) ? { k: 'N', digits: pic.split('V')[0].replace(/[^9]/g, '').length }
|
|
962
|
-
: null;
|
|
963
|
-
categories.set(field, cat);
|
|
964
|
-
return cat;
|
|
965
|
-
};
|
|
966
1211
|
const plainLiteral = (tok) => tok && tok.t === 'lit' && !tok.prefix;
|
|
967
1212
|
const integerOf = (tok) => (tok && (tok.t === 'num' || tok.t === 'word') && /^\d+$/.test(tok.v) ? tok.v : null);
|
|
968
1213
|
const heldAs = (field, tok) => {
|
|
@@ -980,7 +1225,15 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
980
1225
|
return digits != null ? `N:${Number(digits)}` : null;
|
|
981
1226
|
};
|
|
982
1227
|
const heldBy = new Map();
|
|
983
|
-
|
|
1228
|
+
// A flag moved from a constant item holds that item's value, when the item fills the flag exactly.
|
|
1229
|
+
const heldTok = (a) => {
|
|
1230
|
+
const t = a.tok;
|
|
1231
|
+
if (!t || t.t !== 'word' || /^\d+$/.test(t.v)) return t;
|
|
1232
|
+
const src = resolve(t);
|
|
1233
|
+
if (!src || src.index || (src.level !== 78 && src.size !== a.field.size)) return t;
|
|
1234
|
+
return { t: 'lit', v: a.value };
|
|
1235
|
+
};
|
|
1236
|
+
const held = (a) => { if (!heldBy.has(a)) heldBy.set(a, heldAs(a.field, heldTok(a))); return heldBy.get(a); };
|
|
984
1237
|
const flagFields = (tree, out = new Set()) => {
|
|
985
1238
|
if (tree.k === 'not') flagFields(tree.a, out);
|
|
986
1239
|
else if (tree.k === 'and' || tree.k === 'or') { flagFields(tree.a, out); flagFields(tree.b, out); }
|
|
@@ -1074,6 +1327,733 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1074
1327
|
}
|
|
1075
1328
|
}
|
|
1076
1329
|
}
|
|
1330
|
+
// A lower bound survives an increment. A field set to a constant of 1 or more, or varied from one,
|
|
1331
|
+
// stays at 1 or more through ADD, SET UP BY, COMPUTE X = X + n and a VARYING step, where n is
|
|
1332
|
+
// never negative. The upper bound is lost, and the lower bound survives only where the sum cannot
|
|
1333
|
+
// wrap past the field's capacity to a small number: an upper bound live before the increment that
|
|
1334
|
+
// leaves room for n, or ON SIZE ERROR, which leaves the field as it was. Any other write kills it.
|
|
1335
|
+
const intCap = (f) => {
|
|
1336
|
+
if (!f || f.index || f.level === 88 || inTable(f)) return null;
|
|
1337
|
+
const cat = categoryOf(f);
|
|
1338
|
+
return cat && cat.k === 'N' && /^S?9+$/.test(pictureOf(f)) ? 10 ** cat.digits - 1 : null;
|
|
1339
|
+
};
|
|
1340
|
+
// The most a token can add: a literal or constant that is not negative, or an unsigned number.
|
|
1341
|
+
const addend = (tok) => {
|
|
1342
|
+
if (!tok || (tok.t !== 'word' && tok.t !== 'num')) return null;
|
|
1343
|
+
const v = numberOf(constValue([tok]));
|
|
1344
|
+
if (v != null) return v >= 0 ? Math.ceil(v) : null;
|
|
1345
|
+
const f = tok.t === 'word' ? resolve(tok) : null;
|
|
1346
|
+
const cat = f && !f.index && f.level !== 88 && !inTable(f) ? categoryOf(f) : null;
|
|
1347
|
+
return cat && cat.k === 'N' && /^9+(V9*)?$/.test(pictureOf(f)) ? 10 ** cat.digits : null;
|
|
1348
|
+
};
|
|
1349
|
+
// The least a token adds: its value where it is a constant, else nothing.
|
|
1350
|
+
const least = (tok) => { const v = numberOf(constValue([tok])); return v != null && v > 0 ? Math.floor(v) : 0; };
|
|
1351
|
+
const PHRASE_AT = (t) => WORD(t, 'ON') || WORD(t, 'NOT') || WORD(t, 'SIZE') || (t.t === 'word' && t.u.startsWith('END-'));
|
|
1352
|
+
const sizeErrorGuarded = (seg) => seg.some((t, i) => WORD(t, 'SIZE') && WORD(seg[i + 1], 'ERROR') && !WORD(seg[i - 1], 'NOT'));
|
|
1353
|
+
// The fields a statement only ever raises, and by how much at most.
|
|
1354
|
+
const increments = (st) => {
|
|
1355
|
+
const seg = segOf(st);
|
|
1356
|
+
let max = 0;
|
|
1357
|
+
let min = 0;
|
|
1358
|
+
let fields = [];
|
|
1359
|
+
if (st.verb === 'ADD') {
|
|
1360
|
+
const to = seg.findIndex((t) => WORD(t, 'TO'));
|
|
1361
|
+
if (to < 1 || seg.some((t) => WORD(t, 'GIVING') || WORD(t, 'CORRESPONDING') || WORD(t, 'CORR'))) return null;
|
|
1362
|
+
for (const t of seg.slice(0, to)) { const m = addend(t); if (m == null) return null; max += m; min += least(t); }
|
|
1363
|
+
const end = seg.findIndex((t, i) => i > to && PHRASE_AT(t));
|
|
1364
|
+
if (seg.slice(to + 1, end < 0 ? seg.length : end).some((t) => t.t === 'sep')) return null;
|
|
1365
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1366
|
+
} else if (st.verb === 'SET') {
|
|
1367
|
+
const up = seg.findIndex((t) => WORD(t, 'UP'));
|
|
1368
|
+
if (up < 1 || !WORD(seg[up + 1], 'BY') || seg.length !== up + 3 || seg.slice(0, up).some((t) => t.t !== 'word')) return null;
|
|
1369
|
+
max = addend(seg[up + 2]);
|
|
1370
|
+
if (max == null) return null;
|
|
1371
|
+
min = least(seg[up + 2]);
|
|
1372
|
+
fields = (st.targets || []).map((t) => resolve(t));
|
|
1373
|
+
} else if (st.verb === 'COMPUTE') {
|
|
1374
|
+
const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
|
|
1375
|
+
const lhs = seg.slice(0, eq).filter((t) => !WORD(t, 'ROUNDED'));
|
|
1376
|
+
if (eq < 1 || lhs.length !== 1 || lhs[0].t !== 'word') return null;
|
|
1377
|
+
const target = resolve(lhs[0]);
|
|
1378
|
+
const end = seg.findIndex((t, i) => i > eq && PHRASE_AT(t));
|
|
1379
|
+
const parts = [[]];
|
|
1380
|
+
for (const t of seg.slice(eq + 1, end < 0 ? seg.length : end)) { if (t.t === 'op' && t.v === '+') parts.push([]); else parts[parts.length - 1].push(t); }
|
|
1381
|
+
if (parts.length < 2 || parts.some((p) => p.length !== 1)) return null;
|
|
1382
|
+
const self = parts.filter((p) => p[0].t === 'word' && resolve(p[0]) === target);
|
|
1383
|
+
if (self.length !== 1) return null;
|
|
1384
|
+
for (const p of parts) { if (p === self[0]) continue; const m = addend(p[0]); if (m == null) return null; max += m; min += least(p[0]); }
|
|
1385
|
+
fields = [target];
|
|
1386
|
+
} else return null;
|
|
1387
|
+
fields = [...new Set(fields.filter((f) => intCap(f) != null))];
|
|
1388
|
+
return fields.length ? { fields, max, min, sizeError: sizeErrorGuarded(seg) } : null;
|
|
1389
|
+
};
|
|
1390
|
+
// Facts of the form "at least k", one per field and k so that routes which reach it by different
|
|
1391
|
+
// statements agree. They are kept apart because an increment does not kill them.
|
|
1392
|
+
const loOf = new Map();
|
|
1393
|
+
const newLo = (field, lo, at) => {
|
|
1394
|
+
const key = fieldKey(field);
|
|
1395
|
+
const had = (loOf.get(field) || []).find((c) => c.tCons.lo === lo);
|
|
1396
|
+
if (had) return had;
|
|
1397
|
+
noteField(key, field);
|
|
1398
|
+
const c = { id: checks.length, key, field, file: at.file, line: at.line, tCons: { lo, loInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, mono: true };
|
|
1399
|
+
checks.push(c);
|
|
1400
|
+
if (!loOf.has(field)) loOf.set(field, []);
|
|
1401
|
+
loOf.get(field).push(c);
|
|
1402
|
+
return c;
|
|
1403
|
+
};
|
|
1404
|
+
// A field an increment by at least 1 raises can start from 0: the bound it reaches is 1 or more.
|
|
1405
|
+
const raisers = new Map();
|
|
1406
|
+
const raised = new Set();
|
|
1407
|
+
nodes.forEach((n, id) => {
|
|
1408
|
+
if (n.kind !== 'stmt' || !n.st || n.st.at == null) return;
|
|
1409
|
+
const inc = increments(n.st);
|
|
1410
|
+
if (!inc || inc.min < 1 || inc.sizeError) return;
|
|
1411
|
+
raisers.set(id, inc);
|
|
1412
|
+
for (const f of inc.fields) raised.add(f);
|
|
1413
|
+
});
|
|
1414
|
+
for (const a of assigns) {
|
|
1415
|
+
const v = numberOf(a.value);
|
|
1416
|
+
const cap = intCap(a.field);
|
|
1417
|
+
if (cap == null || v == null || !Number.isInteger(v) || v < (raised.has(a.field) ? 0 : 1) || v > cap) continue;
|
|
1418
|
+
if ((a.n.st.targets || []).some((t) => { const o = resolve(t); return o && o !== a.field && overlaps(o, a.field); })) continue;
|
|
1419
|
+
(a.n.genStmt ||= []).push(newLo(a.field, v, a.n.st).t);
|
|
1420
|
+
}
|
|
1421
|
+
// An increment by at least m turns "at least k" into "at least k + m", where the sum fits the field.
|
|
1422
|
+
const lifted = new Map();
|
|
1423
|
+
for (const [id, inc] of raisers) {
|
|
1424
|
+
const list = [];
|
|
1425
|
+
for (const field of inc.fields) {
|
|
1426
|
+
const pairs = [];
|
|
1427
|
+
for (const c of [...(loOf.get(field) || []), ...tallies.filter((t) => t.field === field)]) {
|
|
1428
|
+
const lo = c.tCons.lo + inc.min;
|
|
1429
|
+
if (lo < 1) continue;
|
|
1430
|
+
if (lo <= intCap(field)) pairs.push([c.t, newLo(field, lo, nodes[id].st).t]);
|
|
1431
|
+
}
|
|
1432
|
+
if (pairs.length) list.push({ field, max: inc.max, pairs });
|
|
1433
|
+
}
|
|
1434
|
+
if (list.length) lifted.set(id, list);
|
|
1435
|
+
}
|
|
1436
|
+
// A VARYING counter is at least what it starts at inside the loop: a constant FROM sets it at the
|
|
1437
|
+
// head, and a FROM naming a field that is at least k sets it to k, where that field held it.
|
|
1438
|
+
const stepped = new Map();
|
|
1439
|
+
const derived = new Map();
|
|
1440
|
+
for (let more = true; more;) {
|
|
1441
|
+
more = false;
|
|
1442
|
+
for (const { head, step, loops } of loopHeads) {
|
|
1443
|
+
loops.forEach((loop, idx) => {
|
|
1444
|
+
const field = loop.counter ? resolve(loop.counter) : null;
|
|
1445
|
+
const cap = intCap(field);
|
|
1446
|
+
const by = loop.by ? (loop.by.length === 1 ? addend(loop.by[0]) : null) : 1;
|
|
1447
|
+
if (cap == null || by == null) return;
|
|
1448
|
+
const at = { file: loop.counter.file, line: loop.counter.line };
|
|
1449
|
+
const from = numberOf(constValue(loop.from || []));
|
|
1450
|
+
if (from != null) {
|
|
1451
|
+
const key = `${head}|${idx}`;
|
|
1452
|
+
if (derived.has(key) || !Number.isInteger(from) || from < 1 || from > cap) return;
|
|
1453
|
+
const c = newLo(field, from, at);
|
|
1454
|
+
derived.set(key, c);
|
|
1455
|
+
nodes[head].genStmt = [...(nodes[head].genStmt || []), c.t];
|
|
1456
|
+
if (idx === 0) stepped.set(step, { field, max: by });
|
|
1457
|
+
else if (!loop.until.some((t) => { const v = numberOf(constValue([t])); return v != null && v >= cap; })) nodes[step].genStmt = [...(nodes[step].genStmt || []), c.t];
|
|
1458
|
+
more = true;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const src = idx === 0 && loop.from && loop.from.length === 1 && loop.from[0].t === 'word' ? resolve(loop.from[0]) : null;
|
|
1462
|
+
const srcCap = intCap(src);
|
|
1463
|
+
if (srcCap == null || srcCap > cap) return;
|
|
1464
|
+
for (const c of loOf.get(src) || []) {
|
|
1465
|
+
const key = `${head}|${idx}|${c.tCons.lo}`;
|
|
1466
|
+
let d = derived.get(key);
|
|
1467
|
+
if (!d) {
|
|
1468
|
+
d = newLo(field, c.tCons.lo, at);
|
|
1469
|
+
derived.set(key, d);
|
|
1470
|
+
stepped.set(step, { field, max: by });
|
|
1471
|
+
more = true;
|
|
1472
|
+
}
|
|
1473
|
+
const pair = `${head}|${c.t}>${d.t}`;
|
|
1474
|
+
if (derived.has(pair)) continue;
|
|
1475
|
+
derived.set(pair, true);
|
|
1476
|
+
(nodes[head].implies ||= []).push([c.t, d.t, true]);
|
|
1477
|
+
more = true;
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// A bound an expression gives a field through another field, X <= G + 1, is a bound only where a
|
|
1484
|
+
// check has bounded G: "X <= G + 1 held" and "G <= 100 held" together give X <= 101. The first
|
|
1485
|
+
// fact dies with a write to X or G, the second with a write to G, and the result with a write to X,
|
|
1486
|
+
// so the pair is joined at whichever of the two checks comes last.
|
|
1487
|
+
const outcomeAt = new Map();
|
|
1488
|
+
for (const node of nodes) if (node.kind === 'outcome') outcomeAt.set(`${node.test}|${node.value}`, node);
|
|
1489
|
+
// A backward scan for the last byte past the spaces, VARYING I FROM the field's length BY -1 UNTIL
|
|
1490
|
+
// I < 1 OR F(I:1) > SPACE, leaves I from 1 to that length where F > SPACES held as it began: a
|
|
1491
|
+
// field that compares above spaces has a byte above a space, under any collating sequence, and the
|
|
1492
|
+
// scan meets it before 0. The witness dies with a write to F or to anything its subscript reads.
|
|
1493
|
+
const keptOnStep = new Map();
|
|
1494
|
+
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))));
|
|
1495
|
+
const textOf = (toks) => toks.map((t) => (t.t === 'word' ? t.u : String(t.v))).join(' ');
|
|
1496
|
+
// A whole alphanumeric item or table element, with the fields its subscript reads.
|
|
1497
|
+
const scanned = (toks) => {
|
|
1498
|
+
const base = toks[0] && toks[0].t === 'word' ? resolve(toks[0]) : null;
|
|
1499
|
+
if (!base || base.index || base.level === 88 || base.size == null || categoryOf(base)?.k !== 'A' || variableLength(base)) return null;
|
|
1500
|
+
let i = 1;
|
|
1501
|
+
while (i + 1 < toks.length && (WORD(toks[i], 'OF') || WORD(toks[i], 'IN')) && toks[i + 1].t === 'word') i += 2;
|
|
1502
|
+
const reads = [];
|
|
1503
|
+
if (i < toks.length) {
|
|
1504
|
+
if (toks[i].v !== '(' || toks[toks.length - 1].v !== ')') return null;
|
|
1505
|
+
let depth = 0;
|
|
1506
|
+
for (let j = i; j < toks.length; j++) {
|
|
1507
|
+
const t = toks[j];
|
|
1508
|
+
if (t.v === ':') return null;
|
|
1509
|
+
if (t.v === '(' || t.v === ')') { depth += t.v === '(' ? 1 : -1; if (!depth && j < toks.length - 1) return null; continue; }
|
|
1510
|
+
const f = t.t === 'word' ? resolve(t) : null;
|
|
1511
|
+
if (f) reads.push(f.level === 88 && f.parent ? f.parent : f);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return { base, reads };
|
|
1515
|
+
};
|
|
1516
|
+
// What an outcome says is above spaces (`above`) or only not all spaces.
|
|
1517
|
+
const notBlank = (tree, outcome, out = []) => {
|
|
1518
|
+
if (tree.k === 'not') return notBlank(tree.a, !outcome, out);
|
|
1519
|
+
if ((tree.k === 'and' && outcome) || (tree.k === 'or' && !outcome)) { notBlank(tree.a, outcome, out); notBlank(tree.b, outcome, out); return out; }
|
|
1520
|
+
if (tree.k !== 'rel') return out;
|
|
1521
|
+
const op = outcome ? tree.op : NEGATE[tree.op];
|
|
1522
|
+
if (isSpaces(tree.right) && (op === '>' || op === '<>')) out.push({ toks: tree.left, above: op === '>' });
|
|
1523
|
+
else if (isSpaces(tree.left) && (op === '<' || op === '<>')) out.push({ toks: tree.right, above: op === '<' });
|
|
1524
|
+
return out;
|
|
1525
|
+
};
|
|
1526
|
+
const scans = [];
|
|
1527
|
+
for (const { head, step, test, testAfter, body, loops } of loopHeads) {
|
|
1528
|
+
if (testAfter || !body || loops.length !== 1 || !body.every((el) => el.k === 'stmt' && el.st.verb === 'CONTINUE')) continue;
|
|
1529
|
+
const loop = loops[0];
|
|
1530
|
+
const counter = loop.counter ? resolve(loop.counter) : null;
|
|
1531
|
+
const cap = intCap(counter);
|
|
1532
|
+
const tree = nodes[test].tree;
|
|
1533
|
+
if (cap == null || !tree || tree.k !== 'or' || !loop.by || loop.by.map((t) => t.v).join('') !== '-1') continue;
|
|
1534
|
+
const from = loop.from || [];
|
|
1535
|
+
let start = numberOf(constValue(from));
|
|
1536
|
+
if (start == null && WORD(from[0], 'LENGTH') && WORD(from[1], 'OF')) start = scanned(from.slice(2))?.base.size ?? null;
|
|
1537
|
+
if (start == null || !Number.isInteger(start) || start < 1 || start > cap) continue;
|
|
1538
|
+
const isCounter = (toks) => toks.length === 1 && toks[0].t === 'word' && resolve(toks[0]) === counter;
|
|
1539
|
+
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);
|
|
1540
|
+
const probe = (r) => {
|
|
1541
|
+
if (r.k !== 'rel') return null;
|
|
1542
|
+
const [el, op] = isSpaces(r.right) ? [r.left, r.op] : isSpaces(r.left) ? [r.right, FLIP[r.op]] : [null, null];
|
|
1543
|
+
if (!el || (op !== '>' && op !== '<>') || el.length < 6) return null;
|
|
1544
|
+
const tail = el.slice(-5);
|
|
1545
|
+
if (tail[0].v !== '(' || !isCounter([tail[1]]) || tail[2].v !== ':' || numberOf(constValue([tail[3]])) !== 1 || tail[4].v !== ')') return null;
|
|
1546
|
+
const toks = el.slice(0, -5);
|
|
1547
|
+
const s = scanned(toks);
|
|
1548
|
+
return s && s.base.size === start ? { toks, above: op === '>' } : null;
|
|
1549
|
+
};
|
|
1550
|
+
const p = done(tree.a) ? probe(tree.b) : done(tree.b) ? probe(tree.a) : null;
|
|
1551
|
+
const exit = p && outcomeAt.get(`${test}|true`);
|
|
1552
|
+
if (exit) scans.push({ ...p, counter, start, exit, head, step, at: loop.counter });
|
|
1553
|
+
}
|
|
1554
|
+
// The scan's counter never passes its start: the step runs only once I < 1 has failed, so it
|
|
1555
|
+
// cannot wrap, and the body writes nothing.
|
|
1556
|
+
for (const s of scans) {
|
|
1557
|
+
const key = fieldKey(s.counter);
|
|
1558
|
+
noteField(key, s.counter);
|
|
1559
|
+
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 };
|
|
1560
|
+
checks.push(h);
|
|
1561
|
+
nodes[s.head].genStmt = [...(nodes[s.head].genStmt || []), h.t];
|
|
1562
|
+
keptOnStep.set(s.step, [...(keptOnStep.get(s.step) || []), h.t]);
|
|
1563
|
+
}
|
|
1564
|
+
if (scans.length) {
|
|
1565
|
+
const witness = new Map();
|
|
1566
|
+
const bitFor = (toks, above) => {
|
|
1567
|
+
const k = `${above ? '>' : '<>'}|${textOf(toks)}`;
|
|
1568
|
+
if (witness.has(k)) return witness.get(k);
|
|
1569
|
+
const s = scanned(toks);
|
|
1570
|
+
const bit = nFacts++;
|
|
1571
|
+
for (const f of [s.base, ...s.reads]) {
|
|
1572
|
+
const key = fieldKey(f);
|
|
1573
|
+
noteField(key, f);
|
|
1574
|
+
checks.push({ pseudo: true, key, field: f, x: null, t: bit, f: null });
|
|
1575
|
+
}
|
|
1576
|
+
witness.set(k, bit);
|
|
1577
|
+
return bit;
|
|
1578
|
+
};
|
|
1579
|
+
const wanted = new Set(scans.map((s) => textOf(s.toks)));
|
|
1580
|
+
for (const id of tests) {
|
|
1581
|
+
const n = nodes[id];
|
|
1582
|
+
if (!n.tree) continue;
|
|
1583
|
+
for (const outcome of [true, false]) {
|
|
1584
|
+
for (const nb of notBlank(n.tree, outcome)) {
|
|
1585
|
+
if (!wanted.has(textOf(nb.toks)) || !scanned(nb.toks)) continue;
|
|
1586
|
+
n.gen[String(outcome)].push(bitFor(nb.toks, false));
|
|
1587
|
+
if (nb.above) n.gen[String(outcome)].push(bitFor(nb.toks, true));
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
for (const s of scans) {
|
|
1592
|
+
const bit = witness.get(`${s.above ? '>' : '<>'}|${textOf(s.toks)}`);
|
|
1593
|
+
if (bit == null) continue;
|
|
1594
|
+
const key = fieldKey(s.counter);
|
|
1595
|
+
noteField(key, s.counter);
|
|
1596
|
+
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 } };
|
|
1597
|
+
checks.push(y);
|
|
1598
|
+
(s.exit.implies ||= []).push([bit, y.t]);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const MAX_BOUNDS_PER_RELATION = 8;
|
|
1602
|
+
for (const c of [...checks]) {
|
|
1603
|
+
if (c.pseudo || c.test == null) continue;
|
|
1604
|
+
for (const outcome of [true, false]) {
|
|
1605
|
+
const bit = outcome ? c.rt : c.rf;
|
|
1606
|
+
const dep = bit != null ? (outcome ? c.tDep : c.fDep) : null;
|
|
1607
|
+
if (!dep) continue;
|
|
1608
|
+
const gKey = fieldKey(dep.g);
|
|
1609
|
+
const bounds = [];
|
|
1610
|
+
for (const u of checks) {
|
|
1611
|
+
if (u.pseudo || u.key !== gKey || u.bounds || u.assigned || u === c) continue;
|
|
1612
|
+
for (const [ubit, ucons, uoutcome] of [[u.t, u.tCons, true], [u.f, u.fCons, false]]) {
|
|
1613
|
+
if (ubit == null || !ucons || ucons.dep) continue;
|
|
1614
|
+
const r = asRange(ucons);
|
|
1615
|
+
const gv = dep.needs === 'hi' ? r.hi : r.lo;
|
|
1616
|
+
if (gv == null) continue;
|
|
1617
|
+
const value = dep.m * gv + dep.b;
|
|
1618
|
+
if (!Number.isFinite(value) || Math.abs(value) > EXPR_LIMIT) continue;
|
|
1619
|
+
bounds.push({ u, ubit, uoutcome, value, inc: dep.inc && r[`${dep.needs}Inc`] !== false });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!bounds.length || bounds.length > MAX_BOUNDS_PER_RELATION) continue;
|
|
1623
|
+
noteField(gKey, dep.g);
|
|
1624
|
+
checks.push({ pseudo: true, key: gKey, field: dep.g, x: null, t: bit, f: null });
|
|
1625
|
+
for (const { u, ubit, uoutcome, value, inc } of bounds) {
|
|
1626
|
+
const y = { id: checks.length, key: c.key, field: c.field, file: c.file, line: c.line, tCons: { [dep.side]: value, [`${dep.side}Inc`]: inc }, fCons: null, restricts: false, x: null, t: nFacts++, f: null };
|
|
1627
|
+
checks.push(y);
|
|
1628
|
+
const here = outcomeAt.get(`${c.test}|${outcome}`);
|
|
1629
|
+
if (here) (here.implies ||= []).push([ubit, y.t]);
|
|
1630
|
+
const there = u.test != null ? outcomeAt.get(`${u.test}|${uoutcome}`) : null;
|
|
1631
|
+
if (there) (there.implies ||= []).push([bit, y.t]);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// ---- what an arithmetic statement leaves in an integer field ----
|
|
1637
|
+
// COMPUTE, ADD, SUBTRACT, MULTIPLY, DIVIDE and a MOVE of a field or function leave the receiving
|
|
1638
|
+
// field inside an interval worked out from what each operand can hold: its picture, the facts that
|
|
1639
|
+
// hold at the statement, the length of an item. The interval is a fact only where the result fits
|
|
1640
|
+
// the receiving field, so nothing was lost to truncation or overflow, and where no divisor can be
|
|
1641
|
+
// zero. Whatever the statement's operands needed to hold is what the fact needs to hold.
|
|
1642
|
+
const BINARY_USAGE = /^(COMP|COMP-4|COMP-5|BINARY)$/;
|
|
1643
|
+
const rangesOf = new Map();
|
|
1644
|
+
// The whole numbers an integer field can hold when read, and those a result may take to be stored whole.
|
|
1645
|
+
const rangeOfField = (f) => {
|
|
1646
|
+
if (!f || f.index || f.level === 88) return null;
|
|
1647
|
+
if (rangesOf.has(f)) return rangesOf.get(f);
|
|
1648
|
+
let out = null;
|
|
1649
|
+
const cat = categoryOf(f);
|
|
1650
|
+
const pic = pictureOf(f);
|
|
1651
|
+
if (cat && cat.k === 'N' && /^S?9+$/.test(pic)) {
|
|
1652
|
+
const D = 10n ** BigInt(cat.digits) - 1n;
|
|
1653
|
+
const signed = pic[0] === 'S';
|
|
1654
|
+
const digits = { lo: signed ? -D : 0n, hi: D };
|
|
1655
|
+
out = { hold: digits, fit: digits };
|
|
1656
|
+
if (BINARY_USAGE.test(String(f.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP'))) {
|
|
1657
|
+
if (f.size >= 1 && f.size <= 8) {
|
|
1658
|
+
const bits = BigInt(f.size * 8);
|
|
1659
|
+
const store = signed ? { lo: -(2n ** (bits - 1n)), hi: 2n ** (bits - 1n) - 1n } : { lo: 0n, hi: 2n ** bits - 1n };
|
|
1660
|
+
out = {
|
|
1661
|
+
hold: { lo: store.lo < digits.lo ? store.lo : digits.lo, hi: store.hi > digits.hi ? store.hi : digits.hi },
|
|
1662
|
+
fit: { lo: store.lo > digits.lo ? store.lo : digits.lo, hi: store.hi < digits.hi ? store.hi : digits.hi },
|
|
1663
|
+
};
|
|
1664
|
+
} else out = null;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
rangesOf.set(f, out);
|
|
1668
|
+
return out;
|
|
1669
|
+
};
|
|
1670
|
+
// The most bytes an item named by these tokens can be, as a subscripted element or as written.
|
|
1671
|
+
const sizeBound = (span) => {
|
|
1672
|
+
const f = span[0] && span[0].t === 'word' ? resolve(span[0]) : null;
|
|
1673
|
+
if (!f || f.index || f.level === 88 || f.size == null || !Number.isInteger(f.size)) return null;
|
|
1674
|
+
if (span.length === 1) return f.contributes || f.size;
|
|
1675
|
+
if (!WORD(span[1], 'OF') && span[1].v === '(' && span[span.length - 1].v === ')' && !span.some((t) => t.t === 'op' && t.v === ':')) {
|
|
1676
|
+
return (f.children || []).some((c) => c.level !== 88) ? Math.ceil(f.size / Math.max(1, f.occurs || 1)) : f.size;
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
};
|
|
1680
|
+
const arithEnv = {
|
|
1681
|
+
field: (tok, subscripted) => {
|
|
1682
|
+
const f = tok.t === 'word' ? resolve(tok) : null;
|
|
1683
|
+
return rangeOfField(f) ? { k: 'field', f, tok, plain: !subscripted && !inTable(f) } : null;
|
|
1684
|
+
},
|
|
1685
|
+
sizeOf: sizeBound,
|
|
1686
|
+
};
|
|
1687
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
1688
|
+
// A check's outcome as whole-number ends, from a range or a set of numbers; null ends are open.
|
|
1689
|
+
const wholeEnds = (cons) => {
|
|
1690
|
+
const isNum = (v) => /^[+-]?\d+(\.\d+)?$/.test(v);
|
|
1691
|
+
if (cons.set && !(cons.set.size && [...cons.set].every(isNum))) return null;
|
|
1692
|
+
const r = cons.set ? asRange(cons) : cons;
|
|
1693
|
+
const end = (v, up, inc) => (v == null || Math.abs(v) >= Number.MAX_SAFE_INTEGER ? null : BigInt(up ? (inc ? Math.floor(v) : Math.ceil(v) - 1) : (inc ? Math.ceil(v) : Math.floor(v) + 1)));
|
|
1694
|
+
const lo = end(r.lo, false, r.loInc);
|
|
1695
|
+
const hi = end(r.hi, true, r.hiInc);
|
|
1696
|
+
return lo == null && hi == null ? null : { lo, hi };
|
|
1697
|
+
};
|
|
1698
|
+
let factsByKey = new Map();
|
|
1699
|
+
const indexFacts = () => {
|
|
1700
|
+
factsByKey = new Map();
|
|
1701
|
+
for (const c of checks) {
|
|
1702
|
+
if (c.pseudo || c.key == null || (!c.tCons && !c.fCons)) continue;
|
|
1703
|
+
if (!factsByKey.has(c.key)) factsByKey.set(c.key, []);
|
|
1704
|
+
factsByKey.get(c.key).push(c);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
// The facts held where the statement being read runs, from the last analysis; none on the first pass.
|
|
1708
|
+
let heldBits = null;
|
|
1709
|
+
// What a field can hold, and the facts that say so: its picture, or a constant, or the tightest
|
|
1710
|
+
// lower and upper end among the facts that hold here.
|
|
1711
|
+
const leafCands = (leaf) => {
|
|
1712
|
+
const r = rangeOfField(leaf.f);
|
|
1713
|
+
const base = { needs: [], lo: r.hold.lo, hi: r.hold.hi };
|
|
1714
|
+
if (!leaf.plain) return [base];
|
|
1715
|
+
const cv = numberOf(constValue([leaf.tok]));
|
|
1716
|
+
if (cv != null && Number.isInteger(cv)) return [{ needs: [], lo: BigInt(cv), hi: BigInt(cv) }];
|
|
1717
|
+
if (!heldBits) return [base];
|
|
1718
|
+
let lo = null;
|
|
1719
|
+
let hi = null;
|
|
1720
|
+
for (const c of factsByKey.get(fieldKey(leaf.f)) || []) {
|
|
1721
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
1722
|
+
if (bit == null || !cons || !hasFact(heldBits, bit)) continue;
|
|
1723
|
+
const e = wholeEnds(cons);
|
|
1724
|
+
if (!e) continue;
|
|
1725
|
+
if (e.lo != null && e.lo > (lo ? lo.v : base.lo)) lo = { bit, v: e.lo };
|
|
1726
|
+
if (e.hi != null && e.hi < (hi ? hi.v : base.hi)) hi = { bit, v: e.hi };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const needs = [...new Set([lo && lo.bit, hi && hi.bit].filter((x) => x != null))].sort((x, y) => x - y);
|
|
1730
|
+
const from = lo ? lo.v : base.lo;
|
|
1731
|
+
const to = hi ? hi.v : base.hi;
|
|
1732
|
+
return needs.length && from <= to ? [base, { needs, lo: from, hi: to }] : [base];
|
|
1733
|
+
};
|
|
1734
|
+
// A fact "the field holds a whole number from lo to hi" (either end may be open), one per field and interval.
|
|
1735
|
+
const derivedOf = new Map();
|
|
1736
|
+
const arithBudget = () => nodes.length * Math.ceil((nFacts + 64) / 32) * 4 <= FACT_BUDGET_BYTES / 2;
|
|
1737
|
+
const derivedFact = (field, lo, hi, at) => {
|
|
1738
|
+
if (lo != null && (lo > MAX_SAFE || lo < -MAX_SAFE)) lo = null;
|
|
1739
|
+
if (hi != null && (hi > MAX_SAFE || hi < -MAX_SAFE)) hi = null;
|
|
1740
|
+
if (lo == null && hi == null) return null;
|
|
1741
|
+
// Each end is a fact of its own, so that routes which reach it by different statements meet at the looser.
|
|
1742
|
+
if (lo != null && hi != null) { derivedFact(field, lo, null, at); derivedFact(field, null, hi, at); }
|
|
1743
|
+
const id = `${lo ?? ''}|${hi ?? ''}`;
|
|
1744
|
+
if (!derivedOf.has(field)) derivedOf.set(field, new Map());
|
|
1745
|
+
const mine = derivedOf.get(field);
|
|
1746
|
+
if (mine.has(id)) return mine.get(id);
|
|
1747
|
+
if (!arithBudget()) return null;
|
|
1748
|
+
const key = fieldKey(field);
|
|
1749
|
+
noteField(key, field);
|
|
1750
|
+
const bounds = { ...(lo != null ? { lo: Number(lo) } : {}), ...(hi != null ? { hi: Number(hi) } : {}) };
|
|
1751
|
+
const tCons = { numeric: true, ...(lo != null ? { lo: Number(lo), loInc: true } : {}), ...(hi != null ? { hi: Number(hi), hiInc: true } : {}) };
|
|
1752
|
+
const c = { id: checks.length, key, field, file: at.file, line: at.line, tCons, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds, derived: true };
|
|
1753
|
+
checks.push(c);
|
|
1754
|
+
mine.set(id, c);
|
|
1755
|
+
return c;
|
|
1756
|
+
};
|
|
1757
|
+
const derivedSeen = new Set();
|
|
1758
|
+
const rules = [];
|
|
1759
|
+
// Whatever holds after `n` when the facts in `needs` hold before it; with none needed, whenever it runs.
|
|
1760
|
+
const derive = (n, needs, fact, bucket = null) => {
|
|
1761
|
+
const id = `${n.id}|${bucket}|${needs.join(',')}|${fact.t}`;
|
|
1762
|
+
if (derivedSeen.has(id)) return false;
|
|
1763
|
+
derivedSeen.add(id);
|
|
1764
|
+
rules.push({ n, needs, fact, bucket });
|
|
1765
|
+
return true;
|
|
1766
|
+
};
|
|
1767
|
+
// An interval implies every looser interval the model holds about its field, so a route that reached a
|
|
1768
|
+
// tighter one and a route that reached a looser one still meet at the looser. A check the program made
|
|
1769
|
+
// is not among them: the interval is what the data holds, not a test that ran.
|
|
1770
|
+
const withLooser = (fact) => {
|
|
1771
|
+
const out = [fact.t];
|
|
1772
|
+
const own = fact.bounds;
|
|
1773
|
+
for (const c of factsByKey.get(fact.key) || []) {
|
|
1774
|
+
if (c === fact || !c.derived) continue;
|
|
1775
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
1776
|
+
if (bit == null || !cons) continue;
|
|
1777
|
+
const e = wholeEnds(cons);
|
|
1778
|
+
if (!e) continue;
|
|
1779
|
+
if ((e.lo == null || (own.lo != null && BigInt(own.lo) >= e.lo)) && (e.hi == null || (own.hi != null && BigInt(own.hi) <= e.hi))) out.push(bit);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return out;
|
|
1783
|
+
};
|
|
1784
|
+
const expandRules = () => {
|
|
1785
|
+
indexFacts();
|
|
1786
|
+
for (const n of nodes) { n.derive = undefined; n.genArith = undefined; }
|
|
1787
|
+
for (const r of rules) {
|
|
1788
|
+
const bits = withLooser(r.fact);
|
|
1789
|
+
const at = r.bucket == null ? r.n : nodes[r.n.test];
|
|
1790
|
+
if (!r.needs.length) {
|
|
1791
|
+
if (r.bucket == null) (at.genArith ||= []).push(...bits);
|
|
1792
|
+
else ((at.genArithOn ||= { true: [], false: [] })[r.bucket]).push(...bits);
|
|
1793
|
+
} else (r.n.derive ||= []).push({ needs: r.needs, to: bits });
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
const ARITH_VERBS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'MOVE']);
|
|
1798
|
+
const between = (seg, from, to) => seg.slice(from, to < 0 ? seg.length : to);
|
|
1799
|
+
const idxOf = (seg, word) => seg.findIndex((t) => WORD(t, word));
|
|
1800
|
+
// A statement as a tree per receiving field; null where its words are not ones the model reads.
|
|
1801
|
+
const readArith = (st) => {
|
|
1802
|
+
let seg = segOf(st);
|
|
1803
|
+
const cut = seg.findIndex((t) => t.t === 'word' && (t.u === 'ON' || t.u === 'NOT' || t.u === 'SIZE' || t.u === `END-${st.verb}`));
|
|
1804
|
+
if (cut >= 0) seg = seg.slice(0, cut);
|
|
1805
|
+
if (seg.some((t) => WORD(t, 'CORRESPONDING') || WORD(t, 'CORR') || WORD(t, 'REMAINDER'))) return null;
|
|
1806
|
+
const operands = (toks, count) => {
|
|
1807
|
+
const p = parser(toks, arithEnv);
|
|
1808
|
+
const out = [];
|
|
1809
|
+
while (!p.done()) out.push(p.primary());
|
|
1810
|
+
if (!out.length || (count && out.length !== count)) throw parser;
|
|
1811
|
+
return out;
|
|
1812
|
+
};
|
|
1813
|
+
const sum = (list) => list.reduce((a, b) => ({ k: 'bin', op: '+', a, b }));
|
|
1814
|
+
// Receiving names with ROUNDED; anything else, including a subscript, still writes but is not followed.
|
|
1815
|
+
const receivers = (toks) => {
|
|
1816
|
+
const out = [];
|
|
1817
|
+
for (let i = 0; i < toks.length; i++) {
|
|
1818
|
+
const t = toks[i];
|
|
1819
|
+
if (WORD(t, 'ROUNDED')) { if (!out.length) throw parser; out[out.length - 1].rounded = true; if (WORD(toks[i + 1], 'MODE')) i += WORD(toks[i + 2], 'IS') ? 3 : 2; continue; }
|
|
1820
|
+
if (t.t !== 'word') throw parser;
|
|
1821
|
+
const f = resolve(t);
|
|
1822
|
+
out.push({ tok: t, f: f && !f.index && f.level !== 88 ? f : null, rounded: false, sub: toks[i + 1] && toks[i + 1].v === '(' });
|
|
1823
|
+
if (toks[i + 1] && toks[i + 1].v === '(') { const close = closeIn(toks, i + 1); if (close < 0) throw parser; i = close; }
|
|
1824
|
+
}
|
|
1825
|
+
if (!out.length) throw parser;
|
|
1826
|
+
return out;
|
|
1827
|
+
};
|
|
1828
|
+
const closeIn = (toks, i) => { let d = 0; for (let j = i; j < toks.length; j++) { if (toks[j].v === '(') d++; else if (toks[j].v === ')' && --d === 0) return j; } return -1; };
|
|
1829
|
+
const own = (r) => arithEnv.field(r.tok, false);
|
|
1830
|
+
const per = (targets, make) => targets.map((r) => (r.sub || !r.f ? null : make(r)));
|
|
1831
|
+
return attempt(() => {
|
|
1832
|
+
let targets;
|
|
1833
|
+
let trees;
|
|
1834
|
+
switch (st.verb) {
|
|
1835
|
+
case 'COMPUTE': {
|
|
1836
|
+
const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
|
|
1837
|
+
if (eq < 1) return null;
|
|
1838
|
+
targets = receivers(seg.slice(0, eq));
|
|
1839
|
+
const p = parser(seg.slice(eq + 1), arithEnv);
|
|
1840
|
+
const e = p.expr();
|
|
1841
|
+
if (!p.done()) return null;
|
|
1842
|
+
trees = per(targets, () => e);
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
case 'ADD': {
|
|
1846
|
+
const g = idxOf(seg, 'GIVING');
|
|
1847
|
+
if (g < 0) {
|
|
1848
|
+
const to = idxOf(seg, 'TO');
|
|
1849
|
+
if (to < 1) return null;
|
|
1850
|
+
const ops = operands(seg.slice(0, to));
|
|
1851
|
+
targets = receivers(seg.slice(to + 1));
|
|
1852
|
+
trees = per(targets, (r) => { const l = own(r); return l ? sum([...ops, l]) : null; });
|
|
1853
|
+
} else {
|
|
1854
|
+
const ops = operands(seg.slice(0, g).filter((t) => !WORD(t, 'TO')));
|
|
1855
|
+
targets = receivers(seg.slice(g + 1));
|
|
1856
|
+
trees = per(targets, () => sum(ops));
|
|
1857
|
+
}
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
case 'SUBTRACT': {
|
|
1861
|
+
const from = idxOf(seg, 'FROM');
|
|
1862
|
+
const g = idxOf(seg, 'GIVING');
|
|
1863
|
+
if (from < 1) return null;
|
|
1864
|
+
const subs = sum(operands(seg.slice(0, from)));
|
|
1865
|
+
if (g < 0) {
|
|
1866
|
+
targets = receivers(seg.slice(from + 1));
|
|
1867
|
+
trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '-', a: l, b: subs } : null; });
|
|
1868
|
+
} else {
|
|
1869
|
+
const [minuend] = operands(between(seg, from + 1, g), 1);
|
|
1870
|
+
targets = receivers(seg.slice(g + 1));
|
|
1871
|
+
trees = per(targets, () => ({ k: 'bin', op: '-', a: minuend, b: subs }));
|
|
1872
|
+
}
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
case 'MULTIPLY': {
|
|
1876
|
+
const by = idxOf(seg, 'BY');
|
|
1877
|
+
const g = idxOf(seg, 'GIVING');
|
|
1878
|
+
if (by < 1) return null;
|
|
1879
|
+
const [a] = operands(seg.slice(0, by), 1);
|
|
1880
|
+
if (g < 0) {
|
|
1881
|
+
targets = receivers(seg.slice(by + 1));
|
|
1882
|
+
trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '*', a: l, b: a } : null; });
|
|
1883
|
+
} else {
|
|
1884
|
+
const [b] = operands(between(seg, by + 1, g), 1);
|
|
1885
|
+
targets = receivers(seg.slice(g + 1));
|
|
1886
|
+
trees = per(targets, () => ({ k: 'bin', op: '*', a, b }));
|
|
1887
|
+
}
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
case 'DIVIDE': {
|
|
1891
|
+
const into = idxOf(seg, 'INTO');
|
|
1892
|
+
const by = idxOf(seg, 'BY');
|
|
1893
|
+
const g = idxOf(seg, 'GIVING');
|
|
1894
|
+
if (into > 0 && by < 0) {
|
|
1895
|
+
const [d] = operands(seg.slice(0, into), 1);
|
|
1896
|
+
if (g < 0) {
|
|
1897
|
+
targets = receivers(seg.slice(into + 1));
|
|
1898
|
+
trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '/', a: l, b: d } : null; });
|
|
1899
|
+
} else {
|
|
1900
|
+
const [n] = operands(between(seg, into + 1, g), 1);
|
|
1901
|
+
targets = receivers(seg.slice(g + 1));
|
|
1902
|
+
trees = per(targets, () => ({ k: 'bin', op: '/', a: n, b: d }));
|
|
1903
|
+
}
|
|
1904
|
+
} else if (by > 0 && into < 0 && g > by) {
|
|
1905
|
+
const [n] = operands(seg.slice(0, by), 1);
|
|
1906
|
+
const [d] = operands(between(seg, by + 1, g), 1);
|
|
1907
|
+
targets = receivers(seg.slice(g + 1));
|
|
1908
|
+
trees = per(targets, () => ({ k: 'bin', op: '/', a: n, b: d }));
|
|
1909
|
+
} else return null;
|
|
1910
|
+
break;
|
|
1911
|
+
}
|
|
1912
|
+
case 'MOVE': {
|
|
1913
|
+
const to = idxOf(seg, 'TO');
|
|
1914
|
+
if (to < 1) return null;
|
|
1915
|
+
const p = parser(seg.slice(0, to), arithEnv);
|
|
1916
|
+
const e = p.primary();
|
|
1917
|
+
if (!p.done() || (e.k !== 'field' && e.k !== 'range' && e.k !== 'mod')) return null;
|
|
1918
|
+
targets = receivers(seg.slice(to + 1));
|
|
1919
|
+
trees = per(targets, () => e);
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
default: return null;
|
|
1923
|
+
}
|
|
1924
|
+
// Several receivers taking turns: what one takes is no longer what the next reads.
|
|
1925
|
+
if (targets.length > 1) {
|
|
1926
|
+
const written = targets.map((r) => r.f).filter(Boolean);
|
|
1927
|
+
if (written.some((w, i) => written.some((v, j) => i !== j && overlaps(w, v)))) return null;
|
|
1928
|
+
for (const tree of trees) for (const l of tree ? leaves(tree) : []) if (written.some((w) => overlaps(w, l.f))) return null;
|
|
1929
|
+
}
|
|
1930
|
+
return { targets, trees };
|
|
1931
|
+
});
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
const arith = [];
|
|
1935
|
+
try {
|
|
1936
|
+
for (const n of nodes) {
|
|
1937
|
+
if (n.kind !== 'stmt' || !n.st || n.st.at == null || !ARITH_VERBS.has(n.st.verb)) continue;
|
|
1938
|
+
const read = readArith(n.st);
|
|
1939
|
+
if (read && read.trees.some(Boolean)) arith.push({ n, ...read });
|
|
1940
|
+
}
|
|
1941
|
+
} catch {
|
|
1942
|
+
arith.length = 0;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// A condition's requirements that hold whichever way the rest of it went: the relations it makes true.
|
|
1946
|
+
const relationsOf = (tree, outcome, out = []) => {
|
|
1947
|
+
switch (tree.k) {
|
|
1948
|
+
case 'not': return relationsOf(tree.a, !outcome, out);
|
|
1949
|
+
case 'and': if (outcome) { relationsOf(tree.a, true, out); relationsOf(tree.b, true, out); } return out;
|
|
1950
|
+
case 'or': if (!outcome) { relationsOf(tree.a, false, out); relationsOf(tree.b, false, out); } return out;
|
|
1951
|
+
case 'rel': out.push({ rel: tree, pol: outcome }); return out;
|
|
1952
|
+
default: return out;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
const ORDERING = new Set(['<', '<=', '>', '>=']);
|
|
1956
|
+
// Relations between two integer fields: the ordering tests whose sides are both plain fields.
|
|
1957
|
+
const relations = new Map();
|
|
1958
|
+
const collectRelations = (tree, test) => {
|
|
1959
|
+
if (tree.k === 'rel') {
|
|
1960
|
+
if (!ORDERING.has(tree.op) || relations.has(tree)) return;
|
|
1961
|
+
const f = fieldOf(tree.left);
|
|
1962
|
+
const g = fieldOf(tree.right);
|
|
1963
|
+
if (!f || !g || f === g || inTable(f) || inTable(g) || !rangeOfField(f) || !rangeOfField(g)) return;
|
|
1964
|
+
if (constValue(tree.left) != null || constValue(tree.right) != null) return;
|
|
1965
|
+
relations.set(tree, { f, g, op: tree.op, tokF: tree.left[0], tokG: tree.right[0], test, entries: { true: new Map(), false: new Map() } });
|
|
1966
|
+
} else for (const s of [tree.a, tree.b]) if (s) collectRelations(s, test);
|
|
1967
|
+
};
|
|
1968
|
+
try {
|
|
1969
|
+
for (const id of tests) { const t = nodes[id].tree; if (t) collectRelations(t, id); }
|
|
1970
|
+
} catch {
|
|
1971
|
+
relations.clear();
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// The fields that matter: those an index reads, and those that flow into them through the
|
|
1975
|
+
// statements above and the relations between fields.
|
|
1976
|
+
const relevant = new Set();
|
|
1977
|
+
for (const st of prog.statements) for (const x of st.indexes || []) { const it = x.tok && x.tok.t === 'word' ? resolve(x.tok) : null; if (it && !it.index) relevant.add(it); }
|
|
1978
|
+
for (const it of prog.items) {
|
|
1979
|
+
if (!it.dependingOn) continue;
|
|
1980
|
+
const c = resolve({ t: 'word', u: String(it.dependingOn).toUpperCase(), v: it.dependingOn });
|
|
1981
|
+
if (c && !c.index) relevant.add(c);
|
|
1982
|
+
}
|
|
1983
|
+
for (let more = relevant.size > 0; more;) {
|
|
1984
|
+
more = false;
|
|
1985
|
+
const before = relevant.size;
|
|
1986
|
+
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
|
+
for (const r of relations.values()) if (relevant.has(r.f) || relevant.has(r.g)) { relevant.add(r.f); relevant.add(r.g); }
|
|
1988
|
+
more = relevant.size > before;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// One pass over the statements and relations, reading each against the facts `prev` holds at it. True if it added anything.
|
|
1992
|
+
const arithRound = (prev) => {
|
|
1993
|
+
if (!relevant.size || !(arith.length || relations.size)) return false;
|
|
1994
|
+
// Each further pass runs the whole analysis again, so a program too large for that is left with what one pass gives.
|
|
1995
|
+
if (prev && nodes.length * Math.ceil(nFacts / 32) > ARITH_WORK) return false;
|
|
1996
|
+
let grew = false;
|
|
1997
|
+
try {
|
|
1998
|
+
indexFacts();
|
|
1999
|
+
for (const a of arith) {
|
|
2000
|
+
a.trees.forEach((tree, i) => {
|
|
2001
|
+
const r = a.targets[i];
|
|
2002
|
+
if (!tree || !r.f || !rangeOfField(r.f) || inTable(r.f) || !relevant.has(r.f)) return;
|
|
2003
|
+
const fit = rangeOfField(r.f).fit;
|
|
2004
|
+
heldBits = prev ? prev.get(a.n.id) || null : null;
|
|
2005
|
+
for (const c of candidates(tree, leafCands, 64)) {
|
|
2006
|
+
const s = storedRange(c.iv, r.rounded);
|
|
2007
|
+
if (s.lo < fit.lo || s.hi > fit.hi || (s.lo === fit.lo && s.hi === fit.hi)) continue;
|
|
2008
|
+
const fact = derivedFact(r.f, s.lo, s.hi, a.n.st);
|
|
2009
|
+
if (fact && derive(a.n, c.needs, fact)) grew = true;
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
for (const rel of relations.values()) {
|
|
2014
|
+
if (!relevant.has(rel.f) && !relevant.has(rel.g)) continue;
|
|
2015
|
+
const at = nodes[rel.test].at || nodes[rel.test].st;
|
|
2016
|
+
heldBits = prev ? prev.get(rel.test) || null : null;
|
|
2017
|
+
for (const pol of [true, false]) {
|
|
2018
|
+
const eff = pol ? rel.op : NEGATE[rel.op];
|
|
2019
|
+
for (const [tgt, src, tok, op] of [[rel.f, rel.g, rel.tokG, eff], [rel.g, rel.f, rel.tokF, FLIP[eff]]]) {
|
|
2020
|
+
const hold = rangeOfField(tgt).hold;
|
|
2021
|
+
for (const c of leafCands({ f: src, tok, plain: true })) {
|
|
2022
|
+
let lo = null;
|
|
2023
|
+
let hi = null;
|
|
2024
|
+
if (op === '<') hi = c.hi - 1n; else if (op === '<=') hi = c.hi; else if (op === '>') lo = c.lo + 1n; else lo = c.lo;
|
|
2025
|
+
if (lo != null && lo <= hold.lo) lo = null;
|
|
2026
|
+
if (hi != null && hi >= hold.hi) hi = null;
|
|
2027
|
+
if ((lo == null && hi == null) || (hi != null && hi < hold.lo) || (lo != null && lo > hold.hi)) continue;
|
|
2028
|
+
const fact = derivedFact(tgt, lo, hi, at);
|
|
2029
|
+
const id = fact && `${fact.t}|${c.needs}`;
|
|
2030
|
+
if (fact && !rel.entries[pol].has(id)) { rel.entries[pol].set(id, { needs: c.needs, fact }); grew = true; }
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
// A relation holds after the outcome of its test that makes it true, and the fields it bounds hold
|
|
2036
|
+
// what the other side could hold.
|
|
2037
|
+
for (const id of tests) {
|
|
2038
|
+
const n = nodes[id];
|
|
2039
|
+
if (!n.tree) continue;
|
|
2040
|
+
for (const o of [true, false]) {
|
|
2041
|
+
const at = outcomeAt.get(`${id}|${o}`);
|
|
2042
|
+
if (!at) continue;
|
|
2043
|
+
for (const { rel, pol } of relationsOf(n.tree, o)) {
|
|
2044
|
+
const rec = relations.get(rel);
|
|
2045
|
+
if (rec) for (const e of rec.entries[pol].values()) if (derive(at, e.needs, e.fact, String(o))) grew = true;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
} catch {
|
|
2050
|
+
// Whatever went wrong, the statements keep the facts they had.
|
|
2051
|
+
grew = false;
|
|
2052
|
+
}
|
|
2053
|
+
heldBits = null;
|
|
2054
|
+
return grew;
|
|
2055
|
+
};
|
|
2056
|
+
arithRound(null);
|
|
1077
2057
|
// At least one fact, so the analysis runs, and a statement it never reaches has no facts at all.
|
|
1078
2058
|
nFacts ||= 1;
|
|
1079
2059
|
const extraFacts = new Map();
|
|
@@ -1090,7 +2070,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1090
2070
|
}
|
|
1091
2071
|
}
|
|
1092
2072
|
if (!nFacts) return { nodeOf, checks: [], words: 0, facts: new Map(), nodes: nodes.length, extraFacts };
|
|
1093
|
-
|
|
2073
|
+
let words = 0;
|
|
2074
|
+
let facts = new Map();
|
|
2075
|
+
let partial = false;
|
|
2076
|
+
// The analysis proper, run again whenever arithmetic has added facts.
|
|
2077
|
+
const analyse = () => {
|
|
2078
|
+
words = Math.ceil(nFacts / 32);
|
|
2079
|
+
for (const n of nodes) { n.kill = undefined; n.keeps = undefined; n.genS = undefined; n.genArithOn = undefined; }
|
|
2080
|
+
expandRules();
|
|
1094
2081
|
// Every node holds a bitset of every fact, several times over, so the cost is their product. A
|
|
1095
2082
|
// 426,000-line generated program passed an 8 GB heap here, and running out of heap cannot be
|
|
1096
2083
|
// caught. Past the budget the program is left unordered, which callers already handle.
|
|
@@ -1100,19 +2087,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1100
2087
|
const factsOfKey = new Map();
|
|
1101
2088
|
for (const c of checks) {
|
|
1102
2089
|
const list = factsOfKey.get(c.key) || [];
|
|
1103
|
-
for (const f of [c.x, c.t, c.f]) if (f != null) list.push(f);
|
|
2090
|
+
for (const f of [c.x, c.t, c.f, c.rt, c.rf]) if (f != null) list.push(f);
|
|
1104
2091
|
factsOfKey.set(c.key, list);
|
|
1105
2092
|
}
|
|
1106
|
-
const overlaps = (w, f) => {
|
|
1107
|
-
if (w === f) return true;
|
|
1108
|
-
if (w.index || f.index) return w.index && f.index && w.index === f.index;
|
|
1109
|
-
if (w.level === 66 || f.level === 66) return true;
|
|
1110
|
-
if (itemsInRecord(w) !== itemsInRecord(f)) return false;
|
|
1111
|
-
if (w.offset == null || f.offset == null || w.size == null || f.size == null) return true;
|
|
1112
|
-
const wEnd = w.offset + (w.contributes || w.size);
|
|
1113
|
-
const fEnd = f.offset + (f.contributes || f.size);
|
|
1114
|
-
return w.offset < fEnd && f.offset < wEnd;
|
|
1115
|
-
};
|
|
1116
2093
|
const killCache = new Map();
|
|
1117
2094
|
const killOf = (w) => {
|
|
1118
2095
|
if (!w) return null;
|
|
@@ -1128,44 +2105,6 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1128
2105
|
killCache.set(w, bits);
|
|
1129
2106
|
return bits;
|
|
1130
2107
|
};
|
|
1131
|
-
const writtenBy = (n) => {
|
|
1132
|
-
const out = [];
|
|
1133
|
-
const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
|
|
1134
|
-
if (n.st) {
|
|
1135
|
-
for (const t of n.st.targets || []) add(t);
|
|
1136
|
-
if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
|
|
1137
|
-
// A callee may write what it is passed by reference, and a READ fills the file's records.
|
|
1138
|
-
for (const t of n.st.sources || []) {
|
|
1139
|
-
const f = resolve(t);
|
|
1140
|
-
if (!f) continue;
|
|
1141
|
-
if (f.records) for (const r of f.records) out.push(r);
|
|
1142
|
-
else if (n.st.verb === 'CALL') out.push(f);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
if (n.e && n.e.kind === 'SQL') {
|
|
1147
|
-
// A statement fills the host variables of its INTO list and reads the rest.
|
|
1148
|
-
const toks = n.e.toks;
|
|
1149
|
-
const into = toks.findIndex((t) => WORD(t, 'INTO'));
|
|
1150
|
-
if (into >= 0) {
|
|
1151
|
-
for (let i = into + 1; i < toks.length && !WORD(toks[i], 'FROM'); i++) {
|
|
1152
|
-
if (toks[i].t === 'op' && toks[i].v === ':' && toks[i + 1]) add(toks[i + 1]);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
} else if (n.e) {
|
|
1156
|
-
// A command may fill what any option is given directly, except what it sends FROM. A name
|
|
1157
|
-
// inside that argument's own parentheses - a subscript, a reference modification - is read.
|
|
1158
|
-
let depth = 0;
|
|
1159
|
-
let option = null;
|
|
1160
|
-
for (let i = 0; i < n.e.toks.length; i++) {
|
|
1161
|
-
const t = n.e.toks[i];
|
|
1162
|
-
if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
|
|
1163
|
-
if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
|
|
1164
|
-
if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
return out;
|
|
1168
|
-
};
|
|
1169
2108
|
for (const n of nodes) {
|
|
1170
2109
|
if (n.kind !== 'stmt' && n.kind !== 'exec' && n.kind !== 'step' && n.kind !== 'call') continue;
|
|
1171
2110
|
let bits = null;
|
|
@@ -1177,13 +2116,64 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1177
2116
|
}
|
|
1178
2117
|
if (bits) n.kill = bits;
|
|
1179
2118
|
}
|
|
2119
|
+
// The facts that say a field is bounded above from a value that leaves room for `max` more.
|
|
2120
|
+
const roomFacts = new Map();
|
|
2121
|
+
const roomFor = (field, max) => {
|
|
2122
|
+
let out = roomFacts.get(field)?.get(max);
|
|
2123
|
+
if (out) return out;
|
|
2124
|
+
out = [];
|
|
2125
|
+
const cap = intCap(field);
|
|
2126
|
+
for (const c of checks) {
|
|
2127
|
+
if (c.key !== field || c.mono || c.pseudo) continue;
|
|
2128
|
+
for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
2129
|
+
const r = bit != null && cons ? asRange(cons) : null;
|
|
2130
|
+
if (!r || r.hi == null) continue;
|
|
2131
|
+
if ((r.hiInc ? Math.floor(r.hi) : Math.ceil(r.hi) - 1) + max <= cap) out.push(bit);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (!roomFacts.has(field)) roomFacts.set(field, new Map());
|
|
2135
|
+
roomFacts.get(field).set(max, out);
|
|
2136
|
+
return out;
|
|
2137
|
+
};
|
|
2138
|
+
// What a write does not kill: the lower bounds of a field it only raises, where nothing else it
|
|
2139
|
+
// writes shares the field's bytes.
|
|
2140
|
+
const keepAcross = (n, list) => {
|
|
2141
|
+
const wrote = writtenBy(n);
|
|
2142
|
+
for (const { field, max, sizeError } of list) {
|
|
2143
|
+
const mine = loOf.get(field);
|
|
2144
|
+
if (!mine || wrote.some((w) => w !== field && overlaps(w, field))) continue;
|
|
2145
|
+
const guards = sizeError ? null : roomFor(field, max);
|
|
2146
|
+
if (guards && !guards.length) continue;
|
|
2147
|
+
const bits = new Uint32Array(words);
|
|
2148
|
+
for (const c of mine) bits[c.t >>> 5] |= 1 << (c.t & 31);
|
|
2149
|
+
(n.keeps ||= []).push({ bits, guards });
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
nodes.forEach((n, id) => {
|
|
2153
|
+
if (!n.kill) return;
|
|
2154
|
+
if (n.kind === 'stmt' && n.st && n.st.at != null) {
|
|
2155
|
+
const inc = increments(n.st);
|
|
2156
|
+
if (inc) keepAcross(n, inc.fields.map((field) => ({ field, max: inc.max, sizeError: inc.sizeError })));
|
|
2157
|
+
for (const { field, max, pairs } of lifted.get(id) || []) {
|
|
2158
|
+
const guards = roomFor(field, max);
|
|
2159
|
+
if (guards.length && !(n.st.targets || []).some((t) => { const o = resolve(t); return o && o !== field && overlaps(o, field); })) (n.lifts ||= []).push({ pairs, guards });
|
|
2160
|
+
}
|
|
2161
|
+
} else if (n.kind === 'step' && stepped.has(id)) {
|
|
2162
|
+
keepAcross(n, [{ ...stepped.get(id), sizeError: false }]);
|
|
2163
|
+
}
|
|
2164
|
+
if (n.kind === 'step' && keptOnStep.has(id)) {
|
|
2165
|
+
const bits = new Uint32Array(words);
|
|
2166
|
+
for (const b of keptOnStep.get(id)) bits[b >>> 5] |= 1 << (b & 31);
|
|
2167
|
+
(n.keeps ||= []).push({ bits, guards: null });
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
1180
2170
|
|
|
1181
2171
|
// ---- must-analysis ----
|
|
1182
2172
|
const ZERO = new Uint32Array(words);
|
|
1183
2173
|
const ONES = new Uint32Array(words).fill(0xffffffff);
|
|
1184
2174
|
const genBits = (list) => { const b = new Uint32Array(words); for (const x of list) b[x >>> 5] |= 1 << (x & 31); return b; };
|
|
1185
|
-
for (const id of tests) { const n = nodes[id]; n.genT = genBits(n.gen.true); n.genF = genBits(n.gen.false); }
|
|
1186
|
-
for (const n of nodes) if (n.genStmt) n.genS = genBits(n.genStmt);
|
|
2175
|
+
for (const id of tests) { const n = nodes[id]; n.genT = genBits([...n.gen.true, ...(n.genArithOn ? n.genArithOn.true : [])]); n.genF = genBits([...n.gen.false, ...(n.genArithOn ? n.genArithOn.false : [])]); }
|
|
2176
|
+
for (const n of nodes) if (n.genStmt || n.genArith) n.genS = genBits([...(n.genStmt || []), ...(n.genArith || [])]);
|
|
1187
2177
|
const summaries = new Map();
|
|
1188
2178
|
// Reverse postorder from every way in, so a forward analysis visits a node after what precedes it
|
|
1189
2179
|
// and a join is revisited only when a loop brings something new.
|
|
@@ -1209,6 +2199,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1209
2199
|
for (let i = 0; i < post.length; i++) order[post[i]] = post.length - 1 - i;
|
|
1210
2200
|
}
|
|
1211
2201
|
const scratch = new Uint32Array(words);
|
|
2202
|
+
const killScratch = new Uint32Array(words);
|
|
2203
|
+
// A summary is joined to whatever enters the range, so it cannot assume a guard the caller lacks.
|
|
2204
|
+
let summarising = false;
|
|
1212
2205
|
// What holds after a node, in a buffer reused by every call: the caller merges it into the next
|
|
1213
2206
|
// node's facts before calling again. Null where the route ends here.
|
|
1214
2207
|
const transfer = (n, input) => {
|
|
@@ -1226,17 +2219,46 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1226
2219
|
out = scratch;
|
|
1227
2220
|
}
|
|
1228
2221
|
if (n.kill) {
|
|
1229
|
-
|
|
1230
|
-
|
|
2222
|
+
let kill = n.kill;
|
|
2223
|
+
if (n.keeps) {
|
|
2224
|
+
kill = killScratch;
|
|
2225
|
+
kill.set(n.kill);
|
|
2226
|
+
for (const k of n.keeps) {
|
|
2227
|
+
if (k.guards && (summarising || !k.guards.some((g) => input[g >>> 5] & (1 << (g & 31))))) continue;
|
|
2228
|
+
for (let i = 0; i < words; i++) kill[i] &= ~k.bits[i];
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] & ~kill[i]; out = scratch; }
|
|
2232
|
+
else for (let i = 0; i < words; i++) out[i] &= ~kill[i];
|
|
1231
2233
|
}
|
|
1232
2234
|
// What the statement assigns holds after it, over whatever the write took away.
|
|
1233
2235
|
if (n.genS) {
|
|
1234
2236
|
if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] | n.genS[i]; out = scratch; }
|
|
1235
2237
|
else for (let i = 0; i < words; i++) out[i] |= n.genS[i];
|
|
1236
2238
|
}
|
|
2239
|
+
// An increment that cannot wrap raises the lower bounds that held before it.
|
|
2240
|
+
if (n.lifts) {
|
|
2241
|
+
for (const { pairs, guards } of n.lifts) {
|
|
2242
|
+
if (summarising || !guards.some((g) => input[g >>> 5] & (1 << (g & 31)))) continue;
|
|
2243
|
+
for (const [from, to] of pairs) {
|
|
2244
|
+
if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
|
|
2245
|
+
if (out === input) { scratch.set(input); out = scratch; }
|
|
2246
|
+
out[to >>> 5] |= 1 << (to & 31);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
// What a statement or a test leaves in a field, where the facts its operands needed held before it.
|
|
2251
|
+
if (n.derive && !summarising) {
|
|
2252
|
+
for (const d of n.derive) {
|
|
2253
|
+
if (!d.needs.every((x) => input[x >>> 5] & (1 << (x & 31)))) continue;
|
|
2254
|
+
if (out === input) { scratch.set(input); out = scratch; }
|
|
2255
|
+
for (const t of d.to) out[t >>> 5] |= 1 << (t & 31);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
1237
2258
|
// A flag test's outcome turns what held before it into the bound the flag stood for.
|
|
1238
2259
|
if (n.implies) {
|
|
1239
|
-
for (const [from, to] of n.implies) {
|
|
2260
|
+
for (const [from, to, soft] of n.implies) {
|
|
2261
|
+
if (soft && summarising) continue;
|
|
1240
2262
|
if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
|
|
1241
2263
|
if (out === input) { scratch.set(input); out = scratch; }
|
|
1242
2264
|
out[to >>> 5] |= 1 << (to & 31);
|
|
@@ -1310,6 +2332,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1310
2332
|
// a recursive PERFORM does anyway.
|
|
1311
2333
|
const performed = [...ranges.values()].filter((r) => r.entry != null && r.end != null);
|
|
1312
2334
|
const callers = new Map();
|
|
2335
|
+
summarising = true;
|
|
1313
2336
|
const pending = [...performed].sort((a, b) => order[b.entry] - order[a.entry]);
|
|
1314
2337
|
const queued = new Set(pending.map((r) => r.key));
|
|
1315
2338
|
for (let steps = 0; pending.length && steps < performed.length * 20; steps++) {
|
|
@@ -1327,7 +2350,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1327
2350
|
for (const c of callers.get(r.key) || []) if (!queued.has(c.key)) { queued.add(c.key); pending.push(c); }
|
|
1328
2351
|
}
|
|
1329
2352
|
// Stopped at a step limit, a summary may say a range never returns when it does.
|
|
1330
|
-
|
|
2353
|
+
partial = pending.length > 0;
|
|
2354
|
+
summarising = false;
|
|
1331
2355
|
|
|
1332
2356
|
// Contexts: the program's own entry and every extra one enter with nothing; a range enters with
|
|
1333
2357
|
// what holds at every PERFORM of it that some context reaches. A context runs again only when what
|
|
@@ -1359,13 +2383,16 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1359
2383
|
}
|
|
1360
2384
|
}
|
|
1361
2385
|
if (work.length || unknownLabel) partial = true;
|
|
1362
|
-
|
|
2386
|
+
facts = new Map();
|
|
1363
2387
|
for (const IN of results.values()) {
|
|
1364
2388
|
for (const [id, bits] of IN) {
|
|
1365
2389
|
const had = facts.get(id);
|
|
1366
2390
|
if (had) meetInto(had, bits); else facts.set(id, bits.slice());
|
|
1367
2391
|
}
|
|
1368
2392
|
}
|
|
2393
|
+
};
|
|
2394
|
+
analyse();
|
|
2395
|
+
for (let round = 1; round < 12 && arithRound(facts); round++) { analyse(); }
|
|
1369
2396
|
|
|
1370
2397
|
// What a run can reach, from the graph alone and with the contexts the facts use: code entered from
|
|
1371
2398
|
// an entry runs on past the end of a paragraph, and a performed range stops at its end. A PERFORM
|
|
@@ -1423,6 +2450,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1423
2450
|
const guards = reads.get(x.tok);
|
|
1424
2451
|
let said = null;
|
|
1425
2452
|
let ran = null;
|
|
2453
|
+
const chains = guards || [];
|
|
1426
2454
|
for (const g of guards || []) {
|
|
1427
2455
|
let cons = new Map();
|
|
1428
2456
|
const r = new Map();
|
|
@@ -1434,14 +2462,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1434
2462
|
const mine = [];
|
|
1435
2463
|
for (const key of new Set([...said.keys(), ...ran.keys()])) {
|
|
1436
2464
|
const c = { id: checks.length, key, field: ran.get(key) || (typeof key === 'string' ? { index: key.slice(3) } : key), file: (n.at || n.st).file, line: (n.at || n.st).line,
|
|
1437
|
-
tCons: said.get(key)
|
|
2465
|
+
tCons: plainCons(said.get(key)), fCons: null, restricts: ran.has(key), f: null };
|
|
1438
2466
|
c.x = c.restricts ? placeFacts++ : null;
|
|
1439
2467
|
c.t = c.tCons ? placeFacts++ : null;
|
|
1440
2468
|
checks.push(c);
|
|
1441
2469
|
mine.push(c);
|
|
1442
2470
|
}
|
|
1443
2471
|
posOf.set(x.tok, nodes.length + places.length);
|
|
1444
|
-
places.push({ test: id, checks: mine });
|
|
2472
|
+
places.push({ test: id, checks: mine, chains });
|
|
1445
2473
|
}
|
|
1446
2474
|
}
|
|
1447
2475
|
const reachedAt = new Uint8Array(nodes.length + places.length);
|
|
@@ -1453,6 +2481,19 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1453
2481
|
const bits = new Uint32Array(Math.max(words, Math.ceil(placeFacts / 32)));
|
|
1454
2482
|
bits.set(before);
|
|
1455
2483
|
for (const c of p.checks) for (const x of [c.x, c.t]) if (x != null) bits[x >>> 5] |= 1 << (x & 31);
|
|
2484
|
+
// What a relation before the name says of its fields, if every reading of the name is behind one.
|
|
2485
|
+
let related = null;
|
|
2486
|
+
for (const chain of p.chains) {
|
|
2487
|
+
const got = new Set();
|
|
2488
|
+
for (const [sub, outcome] of chain) {
|
|
2489
|
+
for (const { rel, pol } of relationsOf(sub, outcome)) {
|
|
2490
|
+
const rec = relations.get(rel);
|
|
2491
|
+
if (rec) for (const e of rec.entries[pol].values()) if (e.needs.every((x) => before[x >>> 5] & (1 << (x & 31)))) for (const x of withLooser(e.fact)) got.add(x);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
related = related ? new Set([...related].filter((x) => got.has(x))) : got;
|
|
2495
|
+
}
|
|
2496
|
+
for (const x of related || []) bits[x >>> 5] |= 1 << (x & 31);
|
|
1456
2497
|
facts.set(nodes.length + i, bits);
|
|
1457
2498
|
});
|
|
1458
2499
|
|
|
@@ -1460,7 +2501,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
1460
2501
|
// the caller has let the parse tree go. A statement no context reaches has no facts, and credits
|
|
1461
2502
|
// nothing. One `reached` does not mark does not run, unless `partial` says the analysis stopped
|
|
1462
2503
|
// short or met a paragraph the parse did not find.
|
|
1463
|
-
return { nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
|
|
2504
|
+
return { loopBody, nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
|
|
1464
2505
|
}
|
|
1465
2506
|
|
|
1466
2507
|
// For each token a condition reads, what each reading of it waits on: the operands to its left at
|
|
@@ -1489,27 +2530,64 @@ export const hasFact = (bits, x) => x != null && bits != null && (bits[x >>> 5]
|
|
|
1489
2530
|
// named is the one that decided it: one whose outcome alone makes the value safe, if there is one.
|
|
1490
2531
|
// A bound the program's data sets rather than a check it made counts only where it keeps the index
|
|
1491
2532
|
// between 1 and the sink's `limit`.
|
|
1492
|
-
|
|
2533
|
+
// `offset` is the constant the sink adds to the index it names, T(I - 1): the outcomes are judged
|
|
2534
|
+
// shifted by it, and a shifted top is held to the limit.
|
|
2535
|
+
export function creditOf(checks, bits, sinkKind, limit = null, offset = 0, nonNeg = false) {
|
|
1493
2536
|
let tested = null;
|
|
1494
2537
|
let cons = null;
|
|
1495
2538
|
let consBy = null;
|
|
1496
2539
|
let alone = null;
|
|
2540
|
+
// An index a check tested is credited only where one outcome of it holds: where both reach the
|
|
2541
|
+
// use, the route on which the check failed reaches it too. An outcome that says something only
|
|
2542
|
+
// through arithmetic on other fields is not one that decided the index.
|
|
2543
|
+
const decided = (c) => !INDEX_SINKS.has(sinkKind) || (hasFact(bits, c.t) && !c.tCons.viaExpr) || (hasFact(bits, c.f) && !c.fCons.viaExpr);
|
|
1497
2544
|
for (const c of checks) {
|
|
1498
|
-
if (c.bounds && !(limit != null && c.bounds.lo >= 1 && c.bounds.hi <= limit)) continue;
|
|
1499
|
-
if (hasFact(bits, c.x) && !tested) tested = c;
|
|
2545
|
+
if (c.bounds && !(limit != null && (c.bounds.lo == null ? c.bounds.hi != null : c.bounds.lo + offset >= 1) && (c.bounds.hi == null || c.bounds.hi + offset <= limit))) continue;
|
|
2546
|
+
if (hasFact(bits, c.x) && !tested && decided(c)) tested = c;
|
|
1500
2547
|
for (const [bit, said] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
1501
2548
|
if (!hasFact(bits, bit)) continue;
|
|
1502
2549
|
cons = meet(cons, said);
|
|
1503
|
-
|
|
2550
|
+
// A lower bound the program's data sets is shared by every place that sets it, so a test names the guard.
|
|
2551
|
+
if (!consBy || (consBy.mono && !c.mono)) consBy = c;
|
|
1504
2552
|
if (!alone && stops(said, sinkKind)) alone = c;
|
|
1505
2553
|
}
|
|
1506
2554
|
}
|
|
1507
2555
|
// A check whose outcome puts the index past either end is the use it should have prevented.
|
|
1508
|
-
|
|
1509
|
-
|
|
2556
|
+
const shifted = shift(cons, offset);
|
|
2557
|
+
// An unsigned index moved up by a constant starts at that constant, whatever the checks said of its bottom.
|
|
2558
|
+
if (shifted && offset > 0 && nonNeg && shifted.lo == null && !shifted.set) { shifted.lo = offset; shifted.loInc = true; }
|
|
2559
|
+
if (limit != null && outside(shifted, limit)) return { level: 0, check: null };
|
|
2560
|
+
// A bound above the table's or the field's size lets the index past its end, however the check was written.
|
|
2561
|
+
const pastEnd = !!shifted && limit != null && (sinkKind === 'subscript' || sinkKind === 'reference-modification') && overTop(shifted, limit);
|
|
2562
|
+
if (shifted && stops(shifted, sinkKind) && !pastEnd) return { level: 2, check: alone || consBy, cons: shifted };
|
|
1510
2563
|
return tested ? { level: 1, check: tested } : { level: 0, check: null };
|
|
1511
2564
|
}
|
|
1512
2565
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
2566
|
+
// Whether a constraint keeps an integer at or below `limit`, and at 1 or more where `fromOne`.
|
|
2567
|
+
export function within(said, limit, fromOne = true) {
|
|
2568
|
+
if (!said) return false;
|
|
2569
|
+
const cons = asRange(said);
|
|
2570
|
+
return (!fromOne || atLeastOne(cons)) && cons.hi != null && (cons.hiInc ? cons.hi <= limit : cons.hi <= limit + 1);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
|
|
2574
|
+
// A constraint moved by a constant: the values it allows, each plus `by`.
|
|
2575
|
+
function shift(said, by) {
|
|
2576
|
+
if (!said) return said;
|
|
2577
|
+
const out = { ...said };
|
|
2578
|
+
if (out.set) out.set = new Set([...out.set].map((v) => (/^[+-]?\d+(\.\d+)?$/.test(v) ? String(Number(v) + by) : v)));
|
|
2579
|
+
if (out.lo != null) out.lo += by;
|
|
2580
|
+
if (out.hi != null) out.hi += by;
|
|
2581
|
+
return out;
|
|
2582
|
+
}
|
|
2583
|
+
const overTop = (said, limit) => {
|
|
2584
|
+
const cons = asRange(said);
|
|
2585
|
+
return cons.hi != null && (cons.hi > limit + 1 || (cons.hi === limit + 1 && cons.hiInc));
|
|
2586
|
+
};
|
|
2587
|
+
|
|
2588
|
+
const outside = (said, limit) => {
|
|
2589
|
+
if (!said) return false;
|
|
2590
|
+
const cons = asRange(said);
|
|
2591
|
+
return (cons.lo != null && (cons.lo > limit || (cons.lo === limit && !cons.loInc)))
|
|
2592
|
+
|| (cons.hi != null && (cons.hi < 1 || (cons.hi === 1 && !cons.hiInc)));
|
|
2593
|
+
};
|