@portll/cobolwork 0.0.1 → 0.2.76
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/LICENSE +661 -0
- package/LICENSING.md +93 -0
- package/NOTICE +9 -0
- package/README.md +325 -3
- package/THIRD-PARTY-NOTICES.md +118 -0
- package/bin/cobolwork.mjs +354 -0
- package/lib/advisories.mjs +133 -0
- package/lib/baseline.mjs +154 -0
- package/lib/bms.mjs +453 -0
- package/lib/build.mjs +402 -0
- package/lib/capabilities.mjs +79 -0
- package/lib/cics-commands.mjs +281 -0
- package/lib/compliance.mjs +81 -0
- package/lib/consequence.mjs +139 -0
- package/lib/control.mjs +1515 -0
- package/lib/csd.mjs +77 -0
- package/lib/dataflow.mjs +1506 -0
- package/lib/diff.mjs +344 -0
- package/lib/explain.mjs +145 -0
- package/lib/gate.mjs +383 -0
- package/lib/index.mjs +6 -0
- package/lib/inventory.mjs +79 -0
- package/lib/jcl.mjs +478 -0
- package/lib/kernel/findings.mjs +94 -0
- package/lib/kernel/identity.mjs +216 -0
- package/lib/kernel/memory.mjs +217 -0
- package/lib/kernel/printable.mjs +6 -0
- package/lib/kernel/registry.mjs +79 -0
- package/lib/kernel/ruleset.mjs +72 -0
- package/lib/kernel/source-tree.mjs +159 -0
- package/lib/kev.mjs +27 -0
- package/lib/options.mjs +512 -0
- package/lib/packs.mjs +148 -0
- package/lib/parser.mjs +2055 -0
- package/lib/policy.mjs +163 -0
- package/lib/precompile-cics.mjs +169 -0
- package/lib/precompile.mjs +544 -0
- package/lib/reach.mjs +122 -0
- package/lib/revision.json +1 -0
- package/lib/revision.mjs +89 -0
- package/lib/sarif.mjs +222 -0
- package/lib/scan.mjs +272 -0
- package/lib/sets/build.mjs +234 -0
- package/lib/sets/cics.mjs +306 -0
- package/lib/sets/compile.mjs +187 -0
- package/lib/sets/copybook.mjs +174 -0
- package/lib/sets/flow.mjs +487 -0
- package/lib/sets/hidden.mjs +216 -0
- package/lib/sets/jcl.mjs +440 -0
- package/lib/sets/log.mjs +406 -0
- package/lib/sets/opaque.mjs +102 -0
- package/lib/sets/priv.mjs +322 -0
- package/lib/sets/recon.mjs +267 -0
- package/lib/sets/vendor.mjs +117 -0
- package/lib/sets/web.mjs +327 -0
- package/lib/site.mjs +164 -0
- package/lib/sources.mjs +156 -0
- package/lib/tui/app.mjs +325 -0
- package/lib/tui/keys.mjs +39 -0
- package/lib/tui/model.mjs +96 -0
- package/lib/tui/run.mjs +38 -0
- package/lib/tui/screen.mjs +59 -0
- package/lib/tui/terminal.mjs +46 -0
- package/lib/utilities.mjs +296 -0
- package/lib/version.mjs +15 -0
- package/lib/words.mjs +318 -0
- package/package.json +45 -6
- package/rules/advisories.json +264 -0
- package/rules/compliance-dora.json +2151 -0
- package/rules/compliance-ffiec.json +2134 -0
- package/rules/compliance-nist80053.json +2134 -0
- package/rules/gitleaks-mainframe.toml +57 -0
- package/rules/kev-ids.json +1729 -0
- package/rules/packs/broadcom.json +124 -0
- package/rules/packs/connectdirect.json +116 -0
- package/rules/packs/controlm.json +114 -0
- package/rules/system-layouts.json +28 -0
- package/schema/cobolwork-coverage.schema.json +65 -0
- package/schema/cobolwork.policy.schema.json +54 -0
package/lib/control.mjs
ADDED
|
@@ -0,0 +1,1515 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Statement order, which the data-flow graph does not have.
|
|
3
|
+
//
|
|
4
|
+
// The graph says where a value can go and nothing about when. A check on a field was therefore
|
|
5
|
+
// credited wherever the field was used - where the check had not run yet, and on routes it never
|
|
6
|
+
// ran on. On the corpus that credited a prompt buffer tested IS NUMERIC for one prompt and used,
|
|
7
|
+
// unchecked, for another. And a check could only lower a finding, never clear one, because nothing
|
|
8
|
+
// could say the check ran first and turned the bad value away.
|
|
9
|
+
//
|
|
10
|
+
// This builds each program's procedure division as a control-flow graph - IF, EVALUATE, SEARCH,
|
|
11
|
+
// inline and performed PERFORM, GO TO, NEXT SENTENCE, EXIT, the conditional phrases of READ, CALL and
|
|
12
|
+
// arithmetic, and the statements that end a run - and computes, for every statement, which checks
|
|
13
|
+
// have run on every route to it since the field they test was last written, and what each check's
|
|
14
|
+
// outcome says the field can hold there. A performed range is summarised once and entered with what
|
|
15
|
+
// holds at every PERFORM of it, so a paragraph shared by fifty callers does not blur the order of any
|
|
16
|
+
// of them.
|
|
17
|
+
//
|
|
18
|
+
// What it does not model: control transferred by EXEC CICS HANDLE CONDITION, AID or ABEND, which
|
|
19
|
+
// enters its label from any later command, and declaratives, which enter on an error. Each is
|
|
20
|
+
// entered holding no facts at all, which can only withhold credit. ALTER makes a GO TO reach every
|
|
21
|
+
// paragraph, for the same reason.
|
|
22
|
+
import { segmentEnd } from './parser.mjs';
|
|
23
|
+
|
|
24
|
+
const WORD = (t, u) => t && t.t === 'word' && t.u === u;
|
|
25
|
+
const PHRASED = new Set(['READ', 'WRITE', 'REWRITE', 'DELETE', 'START', 'RETURN', 'CALL', 'COMPUTE', 'ADD', 'SUBTRACT',
|
|
26
|
+
'MULTIPLY', 'DIVIDE', 'STRING', 'UNSTRING', 'ACCEPT', 'DISPLAY', 'INVOKE', 'JSON', 'XML', 'RECEIVE', 'SEND']);
|
|
27
|
+
const PHRASE_WORDS = new Set(['END', 'INVALID', 'EXCEPTION', 'OVERFLOW', 'END-OF-PAGE', 'EOP']);
|
|
28
|
+
// One copy of every node's facts. The analysis keeps a few, so this is a quarter of what it may use.
|
|
29
|
+
const FACT_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
30
|
+
const SETTLE_BUDGET = 20 * 1000 * 1000;
|
|
31
|
+
// Routines whose only effect is to end the run with an abend.
|
|
32
|
+
const ABEND_ROUTINE = /^(CEE3ABD|CEE3AB2|ILBOABN0|ILBOABN)$/i;
|
|
33
|
+
|
|
34
|
+
const opensPhrase = (seg) => seg.some((x, i) => x.t === 'word' && (PHRASE_WORDS.has(x.u) || (x.u === 'ERROR' && seg[i - 1] && seg[i - 1].u === 'SIZE')));
|
|
35
|
+
const LEAVES = new Set(['exit-perform', 'exit-para', 'exit-section', 'goto', 'stop', 'perform']);
|
|
36
|
+
// The words that open a conditional phrase - AT END, NOT ON EXCEPTION, INVALID KEY - and those that
|
|
37
|
+
// can follow them. At a statement boundary they end the phrase before.
|
|
38
|
+
const PHRASE_OPENERS = new Set(['NOT', 'AT', 'ON', 'INVALID', 'EXCEPTION', 'OVERFLOW', 'END-OF-PAGE', 'EOP', 'SIZE', 'END']);
|
|
39
|
+
const PHRASE_FILLER = new Set([...PHRASE_OPENERS, 'KEY', 'ERROR']);
|
|
40
|
+
const LOOP_START = new Set(['VARYING', 'UNTIL', 'WITH', 'TEST', 'FOREVER', 'TIMES']);
|
|
41
|
+
|
|
42
|
+
// ---- facts ------------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
// A check's outcome, as what it says a field can hold: one of a set of literals, a class, a range.
|
|
45
|
+
// Two operations: `meet` is both of two things being true, `join` is either of them.
|
|
46
|
+
function meet(a, b) {
|
|
47
|
+
if (!a) return b;
|
|
48
|
+
if (!b) return a;
|
|
49
|
+
const out = {};
|
|
50
|
+
if (a.set && b.set) out.set = new Set([...a.set].filter((v) => b.set.has(v)));
|
|
51
|
+
else if (a.set || b.set) out.set = new Set(a.set || b.set);
|
|
52
|
+
if (a.numeric || b.numeric) out.numeric = true;
|
|
53
|
+
if (a.alpha || b.alpha) out.alpha = true;
|
|
54
|
+
const lo = pick(a, b, 'lo', Math.max);
|
|
55
|
+
const hi = pick(a, b, 'hi', Math.min);
|
|
56
|
+
if (lo) { out.lo = lo.v; out.loInc = lo.inc; }
|
|
57
|
+
if (hi) { out.hi = hi.v; out.hiInc = hi.inc; }
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function join(a, b) {
|
|
61
|
+
if (!a || !b) return null;
|
|
62
|
+
// A set of numbers is also a range and a class, so either side of a join can say what it bounds:
|
|
63
|
+
// a length clamped to 80 on one branch and tested <= 80 on the other is at most 80 after both.
|
|
64
|
+
a = asRange(a);
|
|
65
|
+
b = asRange(b);
|
|
66
|
+
const out = {};
|
|
67
|
+
if (a.set && b.set) out.set = new Set([...a.set, ...b.set]);
|
|
68
|
+
if (a.numeric && b.numeric) out.numeric = true;
|
|
69
|
+
if (a.alpha && b.alpha) out.alpha = true;
|
|
70
|
+
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
|
+
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).length ? out : null;
|
|
73
|
+
}
|
|
74
|
+
// Whether a known value meets what a check's outcome says of its field.
|
|
75
|
+
function satisfies(cons, v) {
|
|
76
|
+
if (!cons) return false;
|
|
77
|
+
const num = /^[+-]?\d+(\.\d+)?$/.test(v) ? Number(v) : null;
|
|
78
|
+
if (cons.set && !cons.set.has(v)) return false;
|
|
79
|
+
if (cons.numeric && num == null) return false;
|
|
80
|
+
if (cons.alpha && !/^[A-Za-z ]*$/.test(v)) return false;
|
|
81
|
+
if (cons.lo != null && (num == null || (cons.loInc ? num < cons.lo : num <= cons.lo))) return false;
|
|
82
|
+
if (cons.hi != null && (num == null || (cons.hiInc ? num > cons.hi : num >= cons.hi))) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function asRange(c) {
|
|
86
|
+
if (!c.set || !c.set.size || ![...c.set].every((v) => /^[+-]?\d+(\.\d+)?$/.test(v))) return c;
|
|
87
|
+
const nums = [...c.set].map(Number);
|
|
88
|
+
const out = { ...c, numeric: true };
|
|
89
|
+
if (out.lo == null) { out.lo = Math.min(...nums); out.loInc = true; }
|
|
90
|
+
if (out.hi == null) { out.hi = Math.max(...nums); out.hiInc = true; }
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function pick(a, b, k, better) {
|
|
94
|
+
const av = a[k] != null ? { v: a[k], inc: a[`${k}Inc`] } : null;
|
|
95
|
+
const bv = b[k] != null ? { v: b[k], inc: b[`${k}Inc`] } : null;
|
|
96
|
+
if (!av) return bv;
|
|
97
|
+
if (!bv) return av;
|
|
98
|
+
const v = better(av.v, bv.v);
|
|
99
|
+
return v === av.v ? av : bv;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Whether a constraint makes a value safe for what the sink does with it. A value that can only be
|
|
103
|
+
// one of a set of literals is safe everywhere. Digits alone cannot carry a command, a statement, a
|
|
104
|
+
// script or a job, and are exactly what arithmetic needs. Letters and spaces cannot carry markup or a
|
|
105
|
+
// line break, but can spell a command or a program name. A bound is what an index needs.
|
|
106
|
+
export function stops(cons, sinkKind) {
|
|
107
|
+
if (!cons) return false;
|
|
108
|
+
const set = cons.set && cons.set.size > 0;
|
|
109
|
+
const allDigits = set && [...cons.set].every((v) => /^[+-]?\d+(\.\d+)?$/.test(v));
|
|
110
|
+
switch (sinkKind) {
|
|
111
|
+
case 'arithmetic': return cons.numeric === true || allDigits;
|
|
112
|
+
case 'subscript': case 'reference-modification': case 'occurs-depending-count': case 'loop-bound':
|
|
113
|
+
return cons.hi != null || allDigits;
|
|
114
|
+
case 'dynamic-sql': case 'internal-reader': return set || cons.numeric === true;
|
|
115
|
+
case 'web-response': case 'http-header': case 'log': return set || cons.numeric === true || cons.alpha === true;
|
|
116
|
+
case 'os-command': case 'dynamic-program-load': case 'cics-dynamic-transfer': case 'dynamic-file-path':
|
|
117
|
+
case 'queue-name': case 'outbound-host': return set;
|
|
118
|
+
default: return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- conditions --------------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const RELOP = { '=': '=', '>': '>', '<': '<', '>=': '>=', '<=': '<=', '<>': '<>' };
|
|
125
|
+
const CLASS = new Set(['NUMERIC', 'ALPHABETIC', 'ALPHABETIC-LOWER', 'ALPHABETIC-UPPER']);
|
|
126
|
+
const SIGN = new Set(['POSITIVE', 'NEGATIVE', 'ZERO', 'ZEROS', 'ZEROES']);
|
|
127
|
+
const FIGURATIVE = { SPACE: ' ', SPACES: ' ', ZERO: '0', ZEROS: '0', ZEROES: '0', 'LOW-VALUE': '\u0000', 'LOW-VALUES': '\u0000', 'HIGH-VALUE': 'ÿ', 'HIGH-VALUES': 'ÿ', QUOTE: '"', QUOTES: '"' };
|
|
128
|
+
const NEGATE = { '=': '<>', '<>': '=', '>': '<=', '<=': '>', '<': '>=', '>=': '<' };
|
|
129
|
+
const FLIP = { '=': '=', '<>': '<>', '>': '<', '<': '>', '>=': '<=', '<=': '>=' };
|
|
130
|
+
const RESTRICTING_OPS = new Set(['>', '<', '>=', '<=']);
|
|
131
|
+
|
|
132
|
+
// A condition as a tree of AND, OR and NOT over atoms. It reads what COBOL writes - class and sign
|
|
133
|
+
// tests, relations with their words, condition-names, parentheses, and the abbreviated forms where a
|
|
134
|
+
// relation's subject or operator is carried to the next one: X = 'A' OR 'B', X > 1 AND < 9.
|
|
135
|
+
export function parseCondition(toks, resolve) {
|
|
136
|
+
let i = 0;
|
|
137
|
+
let lastSubject = null;
|
|
138
|
+
let lastOp = null;
|
|
139
|
+
const peek = () => toks[i];
|
|
140
|
+
const operand = () => {
|
|
141
|
+
// One operand: an identifier with its qualifiers and subscripts, a literal, a figurative
|
|
142
|
+
// constant, or an arithmetic expression, which constrains nothing and is kept opaque.
|
|
143
|
+
const start = i;
|
|
144
|
+
let depth = 0;
|
|
145
|
+
while (i < toks.length) {
|
|
146
|
+
const t = toks[i];
|
|
147
|
+
if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; if (depth < 0) break; i++; continue; }
|
|
148
|
+
if (depth > 0) { i++; continue; }
|
|
149
|
+
if (t.t === 'op' && RELOP[t.v]) break;
|
|
150
|
+
if (t.t === 'word' && ['AND', 'OR', 'IS', 'NOT', 'GREATER', 'LESS', 'EQUAL', 'EQUALS', 'THAN', 'TO', 'THEN'].includes(t.u)) break;
|
|
151
|
+
if (t.t === 'word' && (CLASS.has(t.u) || SIGN.has(t.u)) && i > start) break;
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
return toks.slice(start, i);
|
|
155
|
+
};
|
|
156
|
+
const relop = () => {
|
|
157
|
+
// [NOT] GREATER THAN [OR EQUAL TO] | LESS ... | EQUAL TO | symbols. Returns the operator with NOT folded in.
|
|
158
|
+
let neg = false;
|
|
159
|
+
if (WORD(peek(), 'NOT')) { neg = true; i++; }
|
|
160
|
+
const t = peek();
|
|
161
|
+
let op = null;
|
|
162
|
+
if (t && t.t === 'op' && RELOP[t.v]) { op = RELOP[t.v]; i++; }
|
|
163
|
+
else if (WORD(t, 'GREATER') || WORD(t, 'LESS')) {
|
|
164
|
+
const base = t.u === 'GREATER' ? '>' : '<';
|
|
165
|
+
i++;
|
|
166
|
+
if (WORD(peek(), 'THAN')) i++;
|
|
167
|
+
if (WORD(peek(), 'OR') && WORD(toks[i + 1], 'EQUAL')) { i += 2; if (WORD(peek(), 'TO')) i++; op = `${base}=`; } else op = base;
|
|
168
|
+
} else if (WORD(t, 'EQUAL') || WORD(t, 'EQUALS')) { i++; if (WORD(peek(), 'TO')) i++; op = '='; }
|
|
169
|
+
if (!op) return null;
|
|
170
|
+
return neg ? NEGATE[op] : op;
|
|
171
|
+
};
|
|
172
|
+
const simple = () => {
|
|
173
|
+
if (WORD(peek(), 'NOT')) { i++; return { k: 'not', a: simple() }; }
|
|
174
|
+
if (peek() && peek().t === 'sep' && peek().v === '(') {
|
|
175
|
+
i++;
|
|
176
|
+
const inner = or();
|
|
177
|
+
if (peek() && peek().t === 'sep' && peek().v === ')') i++;
|
|
178
|
+
return inner;
|
|
179
|
+
}
|
|
180
|
+
// An abbreviated relation carries the last subject, and the last operator if it has none.
|
|
181
|
+
const save = i;
|
|
182
|
+
const op0 = relop();
|
|
183
|
+
if (op0 && lastSubject) {
|
|
184
|
+
const obj = operand();
|
|
185
|
+
lastOp = op0;
|
|
186
|
+
return { k: 'rel', left: lastSubject, op: op0, right: obj };
|
|
187
|
+
}
|
|
188
|
+
i = save;
|
|
189
|
+
const left = operand();
|
|
190
|
+
if (!left.length) { i++; return { k: 'opaque' }; }
|
|
191
|
+
if (WORD(peek(), 'IS')) i++;
|
|
192
|
+
let neg = false;
|
|
193
|
+
if (WORD(peek(), 'NOT') && toks[i + 1] && toks[i + 1].t === 'word' && (CLASS.has(toks[i + 1].u) || SIGN.has(toks[i + 1].u))) { neg = true; i++; }
|
|
194
|
+
const t = peek();
|
|
195
|
+
if (t && t.t === 'word' && CLASS.has(t.u)) { i++; lastSubject = null; return wrap(neg, { k: 'class', subject: left, cls: t.u }); }
|
|
196
|
+
if (t && t.t === 'word' && SIGN.has(t.u)) { i++; lastSubject = null; return wrap(neg, { k: 'sign', subject: left, sign: t.u.startsWith('ZERO') ? 'ZERO' : t.u }); }
|
|
197
|
+
const op = relop();
|
|
198
|
+
if (op) {
|
|
199
|
+
const right = operand();
|
|
200
|
+
lastSubject = left;
|
|
201
|
+
lastOp = op;
|
|
202
|
+
return { k: 'rel', left, op, right };
|
|
203
|
+
}
|
|
204
|
+
// Neither a relation nor a class test: a condition-name, or an operand standing alone after
|
|
205
|
+
// an abbreviated connective, which takes the last subject and operator.
|
|
206
|
+
if (lastSubject && lastOp && !(left.length === 1 && resolve(left[0]) && resolve(left[0]).level === 88)) {
|
|
207
|
+
return { k: 'rel', left: lastSubject, op: lastOp, right: left };
|
|
208
|
+
}
|
|
209
|
+
lastSubject = null;
|
|
210
|
+
return { k: 'cond', subject: left };
|
|
211
|
+
};
|
|
212
|
+
const wrap = (neg, a) => (neg ? { k: 'not', a } : a);
|
|
213
|
+
const and = () => {
|
|
214
|
+
let a = simple();
|
|
215
|
+
while (WORD(peek(), 'AND')) { i++; a = { k: 'and', a, b: simple() }; }
|
|
216
|
+
return a;
|
|
217
|
+
};
|
|
218
|
+
const or = () => {
|
|
219
|
+
let a = and();
|
|
220
|
+
while (WORD(peek(), 'OR')) { i++; a = { k: 'or', a, b: and() }; }
|
|
221
|
+
return a;
|
|
222
|
+
};
|
|
223
|
+
const tree = or();
|
|
224
|
+
return i < toks.length ? { k: 'and', a: tree, b: { k: 'opaque' } } : tree;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- the program -------------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
// `resolve(token)` is the caller's name resolution, which knows qualifiers and index names. It
|
|
230
|
+
// returns an item, an index name as { index: NAME }, or null. `extra(test)`, if given, names facts
|
|
231
|
+
// of the caller's own that a test's outcomes establish - { true: key, false: key } - and they hold
|
|
232
|
+
// from there on, since nothing this module knows of undoes them. Their bits come back in `extraFacts`.
|
|
233
|
+
export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
234
|
+
if (!prog.proc) return null;
|
|
235
|
+
const { tokens, from, to } = prog.proc;
|
|
236
|
+
const stmtAt = new Map();
|
|
237
|
+
for (const st of prog.statements) if (st.at != null && st.verb !== 'WHEN') stmtAt.set(st.at, st);
|
|
238
|
+
const labelAt = new Map();
|
|
239
|
+
for (const l of prog.labels) if (l.at != null) labelAt.set(l.at, l);
|
|
240
|
+
const labelNames = new Set(prog.labels.map((l) => l.name));
|
|
241
|
+
const extraEntries = new Set();
|
|
242
|
+
// A name the parse did not find as a label, or found twice, leaves which code runs unsure.
|
|
243
|
+
let unknownLabel = false;
|
|
244
|
+
const seenLabel = new Set();
|
|
245
|
+
const twice = new Set();
|
|
246
|
+
for (const l of prog.labels) { if (seenLabel.has(l.name)) twice.add(l.name); seenLabel.add(l.name); }
|
|
247
|
+
const sure = (name) => labelNames.has(name) && !twice.has(name);
|
|
248
|
+
const wordsOf = (e) => e.toks.filter((x) => x.t === 'word').map((x) => x.u);
|
|
249
|
+
// Once IGNORE CONDITION has run, a RETURN or XCTL that fails comes back to the next statement, and
|
|
250
|
+
// whether it has run by then is the order of execution, not of the source. A HANDLE CONDITION sends
|
|
251
|
+
// the failure to its label, but which one is in force is as uncertain, so reach assumes it may not.
|
|
252
|
+
const cicsWords = prog.execs.filter((e) => e.kind === 'CICS').map(wordsOf);
|
|
253
|
+
const failuresReturn = cicsWords.some((w) => w[0] === 'IGNORE');
|
|
254
|
+
const failuresHandled = cicsWords.some((w) => w[0] === 'HANDLE' && w[1] === 'CONDITION');
|
|
255
|
+
// ALTER changes where a GO TO goes, so every GO TO may go anywhere and no code is known dead.
|
|
256
|
+
const altered = prog.statements.some((st) => st.verb === 'ALTER');
|
|
257
|
+
|
|
258
|
+
// ---- statements into a tree ----
|
|
259
|
+
let k = from;
|
|
260
|
+
const isWord = (at, u) => WORD(tokens[at], u);
|
|
261
|
+
const condEnd = (start, end) => {
|
|
262
|
+
for (let j = start; j < end; j++) if (isWord(j, 'NEXT') && isWord(j + 1, 'SENTENCE')) return j;
|
|
263
|
+
return end;
|
|
264
|
+
};
|
|
265
|
+
function parseList(stops, phraseStop = false) {
|
|
266
|
+
const out = [];
|
|
267
|
+
while (k < to) {
|
|
268
|
+
const t = tokens[k];
|
|
269
|
+
if (t.t === 'period' || labelAt.has(k)) break;
|
|
270
|
+
if (t.t === 'exec') { out.push(execNode(t)); k++; continue; }
|
|
271
|
+
if (t.t !== 'word') { k++; continue; }
|
|
272
|
+
if (stops.has(t.u)) break;
|
|
273
|
+
if (t.u === 'NEXT' && isWord(k + 1, 'SENTENCE')) { out.push({ k: 'next-sentence' }); k += 2; continue; }
|
|
274
|
+
const st = stmtAt.get(k);
|
|
275
|
+
if (!st) { k++; continue; }
|
|
276
|
+
const node = parseStatement(st, stops);
|
|
277
|
+
// AT END EXIT PERFORM NOT AT END ...: a statement that leaves can still end a phrase.
|
|
278
|
+
if (!node.opensPhrase && LEAVES.has(node.k) && opensPhrase(tokens.slice(st.at + 1, st.end))) node.opensPhrase = true;
|
|
279
|
+
out.push(node);
|
|
280
|
+
if (phraseStop && node.opensPhrase) break;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function execNode(e) {
|
|
285
|
+
const words = wordsOf(e);
|
|
286
|
+
if (e.kind === 'CICS') {
|
|
287
|
+
// With RESP or NOHANDLE, a RETURN or XCTL that fails comes back to the next statement.
|
|
288
|
+
if (words[0] === 'RETURN' || words[0] === 'XCTL') {
|
|
289
|
+
if (failuresReturn || words.some((w) => w === 'RESP' || w === 'RESP2' || w === 'NOHANDLE')) return { k: 'exec', e, mayEnd: true };
|
|
290
|
+
return { k: 'stop', e, ...(failuresHandled ? { mayContinue: true } : {}) };
|
|
291
|
+
}
|
|
292
|
+
if (words[0] === 'ABEND') return { k: 'stop', e };
|
|
293
|
+
if (words[0] === 'HANDLE' || words[0] === 'PUSH') for (const w of words) if (labelNames.has(w)) { extraEntries.add(w); if (twice.has(w)) unknownLabel = true; }
|
|
294
|
+
} else if (e.kind === 'SQL' && words[0] === 'WHENEVER') {
|
|
295
|
+
for (const w of words) if (labelNames.has(w)) { extraEntries.add(w); if (twice.has(w)) unknownLabel = true; }
|
|
296
|
+
}
|
|
297
|
+
return { k: 'exec', e };
|
|
298
|
+
}
|
|
299
|
+
function parseStatement(st, stops) {
|
|
300
|
+
const seg = tokens.slice(st.at + 1, st.end);
|
|
301
|
+
switch (st.verb) {
|
|
302
|
+
case 'IF': {
|
|
303
|
+
const ce = condEnd(st.at + 1, st.end);
|
|
304
|
+
const cond = tokens.slice(st.at + 1, ce);
|
|
305
|
+
k = ce;
|
|
306
|
+
if (isWord(k, 'THEN')) k++;
|
|
307
|
+
const thenList = parseList(new Set([...stops, 'ELSE', 'END-IF']));
|
|
308
|
+
let elseList = [];
|
|
309
|
+
if (isWord(k, 'ELSE')) { k++; elseList = parseList(new Set([...stops, 'END-IF'])); }
|
|
310
|
+
if (isWord(k, 'END-IF')) k++;
|
|
311
|
+
return { k: 'if', st, cond, then: thenList, else: elseList };
|
|
312
|
+
}
|
|
313
|
+
case 'EVALUATE': {
|
|
314
|
+
const subjects = tokens.slice(st.at + 1, st.end);
|
|
315
|
+
k = st.end;
|
|
316
|
+
const groups = [];
|
|
317
|
+
const inner = new Set([...stops, 'WHEN', 'END-EVALUATE']);
|
|
318
|
+
while (isWord(k, 'WHEN')) {
|
|
319
|
+
const g = { conds: [], other: false, body: [], at: tokens[k] };
|
|
320
|
+
while (isWord(k, 'WHEN')) {
|
|
321
|
+
const e = condEnd(k + 1, segmentEnd(tokens, k, to));
|
|
322
|
+
const c = tokens.slice(k + 1, e);
|
|
323
|
+
if (c.length === 1 && c[0].u === 'OTHER') g.other = true; else g.conds.push(c);
|
|
324
|
+
k = e;
|
|
325
|
+
}
|
|
326
|
+
g.body = parseList(inner);
|
|
327
|
+
groups.push(g);
|
|
328
|
+
}
|
|
329
|
+
if (isWord(k, 'END-EVALUATE')) k++;
|
|
330
|
+
return { k: 'evaluate', st, subjects, groups };
|
|
331
|
+
}
|
|
332
|
+
case 'SEARCH': {
|
|
333
|
+
k = st.end;
|
|
334
|
+
const inner = new Set([...stops, 'WHEN', 'END-SEARCH']);
|
|
335
|
+
const atEnd = seg.some((x) => WORD(x, 'END')) ? parseList(inner) : null;
|
|
336
|
+
const whens = [];
|
|
337
|
+
while (isWord(k, 'WHEN')) {
|
|
338
|
+
const e = condEnd(k + 1, segmentEnd(tokens, k, to));
|
|
339
|
+
const cond = tokens.slice(k + 1, e);
|
|
340
|
+
const at = tokens[k];
|
|
341
|
+
k = e;
|
|
342
|
+
whens.push({ cond, at, body: parseList(inner) });
|
|
343
|
+
}
|
|
344
|
+
if (isWord(k, 'END-SEARCH')) k++;
|
|
345
|
+
return { k: 'search', st, atEnd, whens };
|
|
346
|
+
}
|
|
347
|
+
case 'PERFORM': {
|
|
348
|
+
k = st.end;
|
|
349
|
+
const first = seg[0];
|
|
350
|
+
const loop = seg.some((x) => WORD(x, 'VARYING')) ? 'varying' : seg.some((x) => WORD(x, 'UNTIL')) ? 'until' : seg.some((x) => WORD(x, 'TIMES')) ? 'times' : null;
|
|
351
|
+
const testAfter = seg.some((x, i) => WORD(x, 'AFTER') && i > 0 && WORD(seg[i - 1], 'TEST'));
|
|
352
|
+
if (first && first.t === 'word' && labelNames.has(first.u)) {
|
|
353
|
+
const thru = seg[1] && (WORD(seg[1], 'THRU') || WORD(seg[1], 'THROUGH')) && seg[2] ? seg[2].u : null;
|
|
354
|
+
if (!sure(first.u) || (thru && !sure(thru))) unknownLabel = true;
|
|
355
|
+
return { k: 'perform', st, target: first.u, thru, loop, testAfter };
|
|
356
|
+
}
|
|
357
|
+
// A name that is neither a paragraph nor data nor a loop word is a paragraph the parse missed.
|
|
358
|
+
if (first && first.t === 'word' && !stmtAt.has(st.at + 1) && !LOOP_START.has(first.u) && !/^\d+$/.test(first.v) && !resolve(first)) unknownLabel = true;
|
|
359
|
+
const body = parseList(new Set([...stops, 'END-PERFORM']));
|
|
360
|
+
if (isWord(k, 'END-PERFORM')) k++;
|
|
361
|
+
return { k: 'inline', st, body, loop, testAfter };
|
|
362
|
+
}
|
|
363
|
+
case 'GO': {
|
|
364
|
+
k = st.end;
|
|
365
|
+
const words = seg.filter((x) => x.t === 'word' && x.u !== 'TO');
|
|
366
|
+
const dep = words.findIndex((w) => w.u === 'DEPENDING');
|
|
367
|
+
const named = (dep >= 0 ? words.slice(0, dep) : words).map((w) => w.u).filter((n) => n !== 'OF' && n !== 'IN');
|
|
368
|
+
if (named.some((n) => !sure(n))) unknownLabel = true;
|
|
369
|
+
const targets = altered ? [] : named.filter((n) => labelNames.has(n));
|
|
370
|
+
return { k: 'goto', st, targets, depending: dep >= 0 && !altered };
|
|
371
|
+
}
|
|
372
|
+
case 'GOBACK': k = st.end; return { k: 'stop', st };
|
|
373
|
+
case 'STOP': k = st.end; return WORD(seg[0], 'RUN') ? { k: 'stop', st } : { k: 'stmt', st };
|
|
374
|
+
case 'EXIT': {
|
|
375
|
+
k = st.end;
|
|
376
|
+
const next = seg[0];
|
|
377
|
+
// EXIT PROGRAM with no CALL active carries on to the next statement (Language Reference 6.4).
|
|
378
|
+
if (WORD(next, 'PROGRAM')) return { k: 'stop', st, mayContinue: true };
|
|
379
|
+
if (WORD(next, 'METHOD') || WORD(next, 'FUNCTION')) return { k: 'stop', st };
|
|
380
|
+
if (WORD(next, 'PARAGRAPH')) return { k: 'exit-para', st };
|
|
381
|
+
if (WORD(next, 'SECTION')) return { k: 'exit-section', st };
|
|
382
|
+
if (WORD(next, 'PERFORM')) return { k: 'exit-perform', st, cycle: WORD(seg[1], 'CYCLE') };
|
|
383
|
+
return { k: 'stmt', st };
|
|
384
|
+
}
|
|
385
|
+
default: {
|
|
386
|
+
k = st.end;
|
|
387
|
+
if (st.verb === 'CALL' && seg[0] && seg[0].t === 'lit' && ABEND_ROUTINE.test(String(seg[0].v).trim())) return { k: 'stop', st };
|
|
388
|
+
// A SORT or MERGE performs its input procedure, then its output procedure.
|
|
389
|
+
if (st.verb === 'SORT' || st.verb === 'MERGE') {
|
|
390
|
+
const procs = [];
|
|
391
|
+
seg.forEach((x, i) => {
|
|
392
|
+
if (!WORD(x, 'PROCEDURE') || !(WORD(seg[i - 1], 'INPUT') || WORD(seg[i - 1], 'OUTPUT'))) return;
|
|
393
|
+
const at = WORD(seg[i + 1], 'IS') ? i + 2 : i + 1;
|
|
394
|
+
const target = seg[at] && seg[at].t === 'word' ? seg[at].u : null;
|
|
395
|
+
const thru = target && (WORD(seg[at + 1], 'THRU') || WORD(seg[at + 1], 'THROUGH')) && seg[at + 2] ? seg[at + 2].u : null;
|
|
396
|
+
if (target && labelNames.has(target) && (!thru || labelNames.has(thru))) procs.push({ target, thru });
|
|
397
|
+
if (!target || !sure(target) || (thru && !sure(thru))) unknownLabel = true;
|
|
398
|
+
});
|
|
399
|
+
if (procs.length) return { k: 'sort', st, procs };
|
|
400
|
+
}
|
|
401
|
+
if (PHRASED.has(st.verb) && opensPhrase(seg)) {
|
|
402
|
+
const endWord = `END-${st.verb}`;
|
|
403
|
+
const inner = new Set([...stops, endWord, ...PHRASE_OPENERS]);
|
|
404
|
+
const opener = () => k < to && tokens[k].t === 'word' && PHRASE_OPENERS.has(tokens[k].u) && !stmtAt.has(k);
|
|
405
|
+
const bodies = [];
|
|
406
|
+
for (;;) {
|
|
407
|
+
while (k < to && tokens[k].t === 'word' && PHRASE_FILLER.has(tokens[k].u) && !stmtAt.has(k)) k++;
|
|
408
|
+
const body = parseList(inner, true);
|
|
409
|
+
bodies.push(body);
|
|
410
|
+
const last = body[body.length - 1];
|
|
411
|
+
if (!(last && last.opensPhrase) && !opener()) break;
|
|
412
|
+
if (k >= to || tokens[k].t === 'period') break;
|
|
413
|
+
}
|
|
414
|
+
if (isWord(k, endWord)) k++;
|
|
415
|
+
return { k: 'phrased', st, bodies };
|
|
416
|
+
}
|
|
417
|
+
const node = { k: 'stmt', st };
|
|
418
|
+
if (opensPhrase(seg)) node.opensPhrase = true;
|
|
419
|
+
return node;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const paras = [];
|
|
425
|
+
let cur = { name: null, kind: null, section: null, sentences: [] };
|
|
426
|
+
paras.push(cur);
|
|
427
|
+
// A run starts after END DECLARATIVES; the declaratives are entered only on an error.
|
|
428
|
+
let mainPara = cur;
|
|
429
|
+
let section = null;
|
|
430
|
+
let inDeclaratives = false;
|
|
431
|
+
while (k < to) {
|
|
432
|
+
const t = tokens[k];
|
|
433
|
+
if (labelAt.has(k)) {
|
|
434
|
+
const l = labelAt.get(k);
|
|
435
|
+
if (l.kind === 'S') section = l.name;
|
|
436
|
+
cur = { name: l.name, kind: l.kind, section: l.kind === 'S' ? l.name : section, sentences: [], declarative: inDeclaratives };
|
|
437
|
+
if (inDeclaratives) extraEntries.add(l.name);
|
|
438
|
+
paras.push(cur);
|
|
439
|
+
k++;
|
|
440
|
+
while (k < to && tokens[k].t !== 'period') k++;
|
|
441
|
+
k++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (t.t === 'period') { k++; continue; }
|
|
445
|
+
if (WORD(t, 'DECLARATIVES')) { inDeclaratives = true; k++; continue; }
|
|
446
|
+
if (WORD(t, 'END') && isWord(k + 1, 'DECLARATIVES')) {
|
|
447
|
+
inDeclaratives = false;
|
|
448
|
+
k += 2;
|
|
449
|
+
cur = { name: null, kind: null, section: null, sentences: [] };
|
|
450
|
+
paras.push(cur);
|
|
451
|
+
mainPara = cur;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const before = k;
|
|
455
|
+
cur.sentences.push(parseList(new Set()));
|
|
456
|
+
if (k === before) k++;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- the tree into a graph ----
|
|
460
|
+
const nodes = [];
|
|
461
|
+
const entryStatements = [];
|
|
462
|
+
const add = (kind, extra = {}) => { const n = { id: nodes.length, kind, succ: [], ...extra }; nodes.push(n); return n.id; };
|
|
463
|
+
const nodeOf = new Map();
|
|
464
|
+
const EXIT = add('exit');
|
|
465
|
+
const entryOf = new Map();
|
|
466
|
+
const endOf = new Map();
|
|
467
|
+
for (const p of paras) { p.entry = add('label', { name: p.name }); p.end = add('para-end', { name: p.name }); if (p.name && !entryOf.has(p.name)) { entryOf.set(p.name, p.entry); endOf.set(p.name, p.end); } }
|
|
468
|
+
// A section ends where its last paragraph ends.
|
|
469
|
+
const sectionEnd = new Map();
|
|
470
|
+
for (const p of paras) if (p.section) sectionEnd.set(p.section, p.end);
|
|
471
|
+
const tests = [];
|
|
472
|
+
|
|
473
|
+
function lowerList(list, next, ctx) {
|
|
474
|
+
let cont = next;
|
|
475
|
+
for (let i = list.length - 1; i >= 0; i--) cont = lower(list[i], cont, ctx);
|
|
476
|
+
return cont;
|
|
477
|
+
}
|
|
478
|
+
function outcome(test, value, next) { return add('outcome', { test, value, succ: [next] }); }
|
|
479
|
+
function lower(n, next, ctx) {
|
|
480
|
+
switch (n.k) {
|
|
481
|
+
case 'stmt': {
|
|
482
|
+
const id = add('stmt', { st: n.st, succ: [next] });
|
|
483
|
+
nodeOf.set(n.st, id);
|
|
484
|
+
if (n.st.verb === 'ENTRY') entryStatements.push(id);
|
|
485
|
+
return id;
|
|
486
|
+
}
|
|
487
|
+
case 'exec': { const id = add('exec', { e: n.e, succ: n.mayEnd ? [EXIT, next] : [next] }); nodeOf.set(n.e, id); return id; }
|
|
488
|
+
case 'stop': { const id = add('stop', { st: n.st, e: n.e, succ: [EXIT], ...(n.mayContinue ? { cont: next } : {}) }); nodeOf.set(n.st || n.e, id); return id; }
|
|
489
|
+
case 'sort': {
|
|
490
|
+
let cont = next;
|
|
491
|
+
for (let i = n.procs.length - 1; i >= 0; i--) cont = add('call', { st: n.st, range: rangeOf(n.procs[i].target, n.procs[i].thru), succ: [cont] });
|
|
492
|
+
const id = add('stmt', { st: n.st, succ: [cont] });
|
|
493
|
+
nodeOf.set(n.st, id);
|
|
494
|
+
return id;
|
|
495
|
+
}
|
|
496
|
+
case 'next-sentence': return add('nop', { succ: [ctx.sentenceEnd] });
|
|
497
|
+
case 'exit-para': { const id = add('stmt', { st: n.st, succ: [ctx.paraEnd] }); nodeOf.set(n.st, id); return id; }
|
|
498
|
+
case 'exit-section': { const id = add('stmt', { st: n.st, succ: [ctx.sectionEnd || ctx.paraEnd] }); nodeOf.set(n.st, id); return id; }
|
|
499
|
+
case 'exit-perform': { const id = add('stmt', { st: n.st, succ: [n.cycle ? (ctx.loopTest ?? next) : (ctx.loopExit ?? next)] }); nodeOf.set(n.st, id); return id; }
|
|
500
|
+
case 'if': {
|
|
501
|
+
const t = add('test', { st: n.st, cond: n.cond, branches: [n.then, n.else] });
|
|
502
|
+
tests.push(t);
|
|
503
|
+
nodes[t].succ = [outcome(t, true, lowerList(n.then, next, ctx)), outcome(t, false, lowerList(n.else, next, ctx))];
|
|
504
|
+
nodeOf.set(n.st, t);
|
|
505
|
+
return t;
|
|
506
|
+
}
|
|
507
|
+
case 'evaluate': {
|
|
508
|
+
let fallthrough = next;
|
|
509
|
+
const other = n.groups.find((g) => g.other);
|
|
510
|
+
if (other) fallthrough = lowerList(other.body, next, ctx);
|
|
511
|
+
for (let g = n.groups.length - 1; g >= 0; g--) {
|
|
512
|
+
const grp = n.groups[g];
|
|
513
|
+
if (grp.other) continue;
|
|
514
|
+
// The false outcome runs the later groups, WHEN OTHER among them.
|
|
515
|
+
const t = add('test', { st: n.st, evaluate: { subjects: n.subjects, conds: grp.conds }, at: grp.at, branches: [grp.body, { later: n.groups, from: g + 1 }] });
|
|
516
|
+
tests.push(t);
|
|
517
|
+
nodes[t].succ = [outcome(t, true, lowerList(grp.body, next, ctx)), outcome(t, false, fallthrough)];
|
|
518
|
+
fallthrough = t;
|
|
519
|
+
}
|
|
520
|
+
// The EVALUATE statement reads its subjects and writes nothing; its node is the first test.
|
|
521
|
+
nodeOf.set(n.st, fallthrough);
|
|
522
|
+
return fallthrough;
|
|
523
|
+
}
|
|
524
|
+
case 'search': {
|
|
525
|
+
let fallthrough = n.atEnd ? lowerList(n.atEnd, next, ctx) : next;
|
|
526
|
+
for (let w = n.whens.length - 1; w >= 0; w--) {
|
|
527
|
+
const t = add('test', { st: n.st, cond: n.whens[w].cond, at: n.whens[w].at });
|
|
528
|
+
tests.push(t);
|
|
529
|
+
nodes[t].succ = [outcome(t, true, lowerList(n.whens[w].body, next, ctx)), outcome(t, false, fallthrough)];
|
|
530
|
+
fallthrough = t;
|
|
531
|
+
}
|
|
532
|
+
// SEARCH moves its index before it tests anything.
|
|
533
|
+
const id = add('stmt', { st: n.st, succ: [fallthrough] });
|
|
534
|
+
nodeOf.set(n.st, id);
|
|
535
|
+
return id;
|
|
536
|
+
}
|
|
537
|
+
case 'perform': case 'inline': {
|
|
538
|
+
// A loop tests before its body unless it says WITH TEST AFTER. The UNTIL condition is true at
|
|
539
|
+
// the exit, which is what makes PERFORM GET-INPUT UNTIL INPUT-VALID a check.
|
|
540
|
+
const loops = n.st.loops || [];
|
|
541
|
+
const cond = loops.length ? loops[0].until : null;
|
|
542
|
+
const head = add('stmt', { st: n.st });
|
|
543
|
+
nodeOf.set(n.st, head);
|
|
544
|
+
if (!n.loop) {
|
|
545
|
+
const body = n.k === 'perform' ? add('call', { st: n.st, range: rangeOf(n.target, n.thru), succ: [next] }) : lowerList(n.body, next, ctx);
|
|
546
|
+
nodes[head].succ = [body];
|
|
547
|
+
return head;
|
|
548
|
+
}
|
|
549
|
+
const t = add('test', { st: n.st, cond, loop: true });
|
|
550
|
+
if (cond) tests.push(t);
|
|
551
|
+
const step = add('step', { st: n.st, succ: [t] });
|
|
552
|
+
const loopCtx = { ...ctx, loopExit: next, loopTest: step };
|
|
553
|
+
const body = n.k === 'perform' ? add('call', { st: n.st, range: rangeOf(n.target, n.thru), succ: [step] }) : lowerList(n.body, step, loopCtx);
|
|
554
|
+
nodes[t].succ = [outcome(t, true, next), outcome(t, false, body)];
|
|
555
|
+
nodes[head].succ = [n.testAfter ? body : t];
|
|
556
|
+
return head;
|
|
557
|
+
}
|
|
558
|
+
case 'goto': {
|
|
559
|
+
const targets = n.targets.map((name) => entryOf.get(name)).filter((x) => x != null);
|
|
560
|
+
const all = !n.targets.length ? paras.map((p) => p.entry) : targets;
|
|
561
|
+
const id = add('goto', { st: n.st, succ: n.depending ? [...all, next] : all.length ? all : [EXIT] });
|
|
562
|
+
nodeOf.set(n.st, id);
|
|
563
|
+
return id;
|
|
564
|
+
}
|
|
565
|
+
case 'phrased': {
|
|
566
|
+
const branch = add('branch', { succ: [...n.bodies.map((b) => lowerList(b, next, ctx)), next] });
|
|
567
|
+
const id = add('stmt', { st: n.st, succ: [branch] });
|
|
568
|
+
nodeOf.set(n.st, id);
|
|
569
|
+
return id;
|
|
570
|
+
}
|
|
571
|
+
default: return next;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const ranges = new Map();
|
|
575
|
+
function rangeOf(target, thru) {
|
|
576
|
+
const key = `${target}|${thru || ''}`;
|
|
577
|
+
let r = ranges.get(key);
|
|
578
|
+
if (!r) {
|
|
579
|
+
const last = thru || target;
|
|
580
|
+
const lastPara = paras.find((p) => p.name === last);
|
|
581
|
+
const end = lastPara && lastPara.kind === 'S' ? sectionEnd.get(last) : endOf.get(last);
|
|
582
|
+
r = { key, entry: entryOf.get(target), end: end ?? null };
|
|
583
|
+
ranges.set(key, r);
|
|
584
|
+
}
|
|
585
|
+
return r;
|
|
586
|
+
}
|
|
587
|
+
paras.forEach((p, i) => {
|
|
588
|
+
const ctx = { paraEnd: p.end, sectionEnd: p.section ? sectionEnd.get(p.section) : null };
|
|
589
|
+
let cont = p.end;
|
|
590
|
+
for (let s = p.sentences.length - 1; s >= 0; s--) cont = lowerList(p.sentences[s], cont, { ...ctx, sentenceEnd: cont });
|
|
591
|
+
nodes[p.entry].succ = [cont];
|
|
592
|
+
nodes[p.end].succ = [i + 1 < paras.length ? paras[i + 1].entry : EXIT];
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// ---- checks ----
|
|
596
|
+
// The storage an item lies in: its record, or the record an 01 REDEFINES lays itself over.
|
|
597
|
+
const itemsInRecord = (item) => {
|
|
598
|
+
let top = item;
|
|
599
|
+
while (top.parent) top = top.parent;
|
|
600
|
+
for (let hops = 0; top.redefinesItem && hops < 64; hops++) top = top.redefinesItem;
|
|
601
|
+
return top;
|
|
602
|
+
};
|
|
603
|
+
const fieldKey = (f) => (f.index ? `IX:${f.index}` : f);
|
|
604
|
+
const constValue = (toks) => {
|
|
605
|
+
if (toks.length === 3 && WORD(toks[0], 'LENGTH') && WORD(toks[1], 'OF')) {
|
|
606
|
+
const it = resolve(toks[2]);
|
|
607
|
+
return it && !it.index && it.size != null ? String(it.size) : null;
|
|
608
|
+
}
|
|
609
|
+
if (toks.length !== 1) return null;
|
|
610
|
+
const t = toks[0];
|
|
611
|
+
if (t.t === 'lit') return String(t.v).trimEnd();
|
|
612
|
+
if (t.t === 'num' || (t.t === 'word' && /^[+-]?\d+(\.\d+)?$/.test(t.v))) return String(Number(t.v));
|
|
613
|
+
if (t.t === 'word' && FIGURATIVE[t.u] != null) return FIGURATIVE[t.u];
|
|
614
|
+
const it = resolve(t);
|
|
615
|
+
// 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))) {
|
|
617
|
+
const v = it.values[0];
|
|
618
|
+
if (v && v.t === 'lit') return String(v.v).trimEnd();
|
|
619
|
+
if (v && (v.t === 'num' || /^[+-]?\d+(\.\d+)?$/.test(v.v))) return String(Number(v.v));
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
};
|
|
623
|
+
const fieldOf = (toks) => {
|
|
624
|
+
// The subject of a test: an identifier and its qualifiers, nothing else. A subscripted element,
|
|
625
|
+
// a reference modification or an expression says something about part of a field, or none.
|
|
626
|
+
if (!toks.length || toks.some((t) => t.t !== 'word')) return null;
|
|
627
|
+
const f = resolve(toks[0]);
|
|
628
|
+
return f && f.level !== 88 ? f : null;
|
|
629
|
+
};
|
|
630
|
+
const numberOf = (v) => (v != null && /^[+-]?\d+(\.\d+)?$/.test(v) ? Number(v) : null);
|
|
631
|
+
const relCons = (op, v) => {
|
|
632
|
+
if (v == null) return null;
|
|
633
|
+
if (op === '=') return { set: new Set([v]) };
|
|
634
|
+
const n = numberOf(v);
|
|
635
|
+
if (n == null) return null;
|
|
636
|
+
if (op === '>') return { lo: n, loInc: false };
|
|
637
|
+
if (op === '>=') return { lo: n, loInc: true };
|
|
638
|
+
if (op === '<') return { hi: n, hiInc: false };
|
|
639
|
+
if (op === '<=') return { hi: n, hiInc: true };
|
|
640
|
+
return null;
|
|
641
|
+
};
|
|
642
|
+
const condValues = (item) => {
|
|
643
|
+
const vals = [];
|
|
644
|
+
let range = null;
|
|
645
|
+
const v = item.values || [];
|
|
646
|
+
for (let i = 0; i < v.length; i++) {
|
|
647
|
+
const t = v[i];
|
|
648
|
+
if (t.t === 'word' && (t.u === 'THRU' || t.u === 'THROUGH')) continue;
|
|
649
|
+
const val = t.t === 'lit' ? String(t.v).trimEnd() : t.t === 'word' && FIGURATIVE[t.u] != null ? FIGURATIVE[t.u] : String(t.v);
|
|
650
|
+
if (v[i + 1] && v[i + 1].t === 'word' && (v[i + 1].u === 'THRU' || v[i + 1].u === 'THROUGH') && v[i + 2]) {
|
|
651
|
+
const lo = numberOf(val), hi = numberOf(String(v[i + 2].t === 'lit' ? v[i + 2].v : v[i + 2].v));
|
|
652
|
+
range = lo != null && hi != null ? join(range || { lo, loInc: true, hi, hiInc: true }, { lo, loInc: true, hi, hiInc: true }) : { opaque: true };
|
|
653
|
+
i += 2;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
vals.push(val);
|
|
657
|
+
}
|
|
658
|
+
if (range && range.opaque) return null;
|
|
659
|
+
if (range && !vals.length) return range;
|
|
660
|
+
if (range) return join(range, { set: new Set(vals) }) || null;
|
|
661
|
+
return vals.length ? { set: new Set(vals) } : null;
|
|
662
|
+
};
|
|
663
|
+
// What a condition says each field holds when it is `outcome`, and which fields it tests.
|
|
664
|
+
function consOf(tree, outcome) {
|
|
665
|
+
switch (tree.k) {
|
|
666
|
+
case 'not': return consOf(tree.a, !outcome);
|
|
667
|
+
case 'and': return outcome ? meetMaps(consOf(tree.a, true), consOf(tree.b, true)) : joinMaps(consOf(tree.a, false), consOf(tree.b, false));
|
|
668
|
+
case 'or': return outcome ? joinMaps(consOf(tree.a, true), consOf(tree.b, true)) : meetMaps(consOf(tree.a, false), consOf(tree.b, false));
|
|
669
|
+
case 'class': {
|
|
670
|
+
const f = fieldOf(tree.subject);
|
|
671
|
+
if (!f || !outcome) return new Map();
|
|
672
|
+
return new Map([[fieldKey(f), tree.cls === 'NUMERIC' ? { numeric: true } : { alpha: true }]]);
|
|
673
|
+
}
|
|
674
|
+
case 'sign': {
|
|
675
|
+
const f = fieldOf(tree.subject);
|
|
676
|
+
if (!f) return new Map();
|
|
677
|
+
const c = tree.sign === 'ZERO' ? (outcome ? { set: new Set(['0']) } : null)
|
|
678
|
+
: tree.sign === 'POSITIVE' ? (outcome ? { lo: 0, loInc: false } : { hi: 0, hiInc: true })
|
|
679
|
+
: (outcome ? { hi: 0, hiInc: false } : { lo: 0, loInc: true });
|
|
680
|
+
return c ? new Map([[fieldKey(f), c]]) : new Map();
|
|
681
|
+
}
|
|
682
|
+
case 'rel': {
|
|
683
|
+
let f = fieldOf(tree.left);
|
|
684
|
+
let op = tree.op;
|
|
685
|
+
let v = constValue(tree.right);
|
|
686
|
+
if (!f || v == null) {
|
|
687
|
+
const g = fieldOf(tree.right);
|
|
688
|
+
const w = constValue(tree.left);
|
|
689
|
+
if (g && w != null) { f = g; v = w; op = FLIP[op]; } else return new Map();
|
|
690
|
+
}
|
|
691
|
+
const c = relCons(outcome ? op : NEGATE[op], v);
|
|
692
|
+
return c ? new Map([[fieldKey(f), c]]) : new Map();
|
|
693
|
+
}
|
|
694
|
+
case 'cond': {
|
|
695
|
+
if (tree.subject.length !== 1) return new Map();
|
|
696
|
+
const c = resolve(tree.subject[0]);
|
|
697
|
+
if (!c || c.level !== 88 || !c.parent || !outcome) return new Map();
|
|
698
|
+
const cons = condValues(c);
|
|
699
|
+
return cons ? new Map([[fieldKey(c.parent), cons]]) : new Map();
|
|
700
|
+
}
|
|
701
|
+
default: return new Map();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function meetMaps(a, b) {
|
|
705
|
+
const out = new Map(a);
|
|
706
|
+
for (const [f, c] of b) out.set(f, meet(out.get(f), c));
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
function joinMaps(a, b) {
|
|
710
|
+
const out = new Map();
|
|
711
|
+
for (const [f, c] of a) if (b.has(f)) { const j = join(c, b.get(f)); if (j) out.set(f, j); }
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
// The fields a condition restricts rather than compares: class and sign tests, ordering relations
|
|
715
|
+
// and condition-names. Equality chooses a branch; it is credited only where it pins the value.
|
|
716
|
+
function tested(tree, out = new Map()) {
|
|
717
|
+
switch (tree.k) {
|
|
718
|
+
case 'not': return tested(tree.a, out);
|
|
719
|
+
case 'and': case 'or': tested(tree.a, out); return tested(tree.b, out);
|
|
720
|
+
case 'class': case 'sign': { const f = fieldOf(tree.subject); if (f) out.set(fieldKey(f), f); return out; }
|
|
721
|
+
case 'rel': {
|
|
722
|
+
if (!RESTRICTING_OPS.has(tree.op)) return out;
|
|
723
|
+
for (const side of [tree.left, tree.right]) { const f = fieldOf(side); if (f) out.set(fieldKey(f), f); }
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
case 'cond': {
|
|
727
|
+
const c = tree.subject.length === 1 ? resolve(tree.subject[0]) : null;
|
|
728
|
+
if (c && c.level === 88 && c.parent) out.set(fieldKey(c.parent), c.parent);
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
default: return out;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// The fields `tested` finds that a condition evaluates on every route to `outcome`. The right of an
|
|
735
|
+
// AND runs only if the left was true, the right of an OR only if the left was false; whatever the
|
|
736
|
+
// outcome, only the leftmost operand is sure to run.
|
|
737
|
+
function testedWhen(tree, outcome, out = new Map()) {
|
|
738
|
+
switch (tree.k) {
|
|
739
|
+
case 'not': return testedWhen(tree.a, !outcome, out);
|
|
740
|
+
case 'and': return outcome ? testedWhen(tree.b, true, testedWhen(tree.a, true, out)) : testedAlways(tree.a, out);
|
|
741
|
+
case 'or': return outcome ? testedAlways(tree.a, out) : testedWhen(tree.b, false, testedWhen(tree.a, false, out));
|
|
742
|
+
default: return tested(tree, out);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function testedAlways(tree, out) {
|
|
746
|
+
return tree.k === 'not' || tree.k === 'and' || tree.k === 'or' ? testedAlways(tree.a, out) : tested(tree, out);
|
|
747
|
+
}
|
|
748
|
+
const fields = new Map();
|
|
749
|
+
const noteField = (key, f) => { if (!fields.has(key)) fields.set(key, f); };
|
|
750
|
+
// An EVALUATE group is its subjects matched against each WHEN's objects, any of them.
|
|
751
|
+
function evaluateTree(subjects, conds) {
|
|
752
|
+
const also = subjects.some((t) => WORD(t, 'ALSO'));
|
|
753
|
+
if (also) return { k: 'opaque' };
|
|
754
|
+
const isTrue = subjects.length === 1 && (WORD(subjects[0], 'TRUE') || WORD(subjects[0], 'FALSE'));
|
|
755
|
+
let tree = null;
|
|
756
|
+
for (const c of conds) {
|
|
757
|
+
let t;
|
|
758
|
+
if (isTrue) {
|
|
759
|
+
t = parseCondition(c, resolve);
|
|
760
|
+
if (WORD(subjects[0], 'FALSE')) t = { k: 'not', a: t };
|
|
761
|
+
} else if (c.some((x) => WORD(x, 'ANY'))) t = { k: 'opaque' };
|
|
762
|
+
else {
|
|
763
|
+
const neg = WORD(c[0], 'NOT');
|
|
764
|
+
const body = neg ? c.slice(1) : c;
|
|
765
|
+
const thru = body.findIndex((x) => WORD(x, 'THRU') || WORD(x, 'THROUGH'));
|
|
766
|
+
t = thru > 0
|
|
767
|
+
? { k: 'and', a: { k: 'rel', left: subjects, op: '>=', right: body.slice(0, thru) }, b: { k: 'rel', left: subjects, op: '<=', right: body.slice(thru + 1) } }
|
|
768
|
+
: { k: 'rel', left: subjects, op: '=', right: body };
|
|
769
|
+
if (neg) t = { k: 'not', a: t };
|
|
770
|
+
}
|
|
771
|
+
tree = tree ? { k: 'or', a: tree, b: t } : t;
|
|
772
|
+
}
|
|
773
|
+
return tree || { k: 'opaque' };
|
|
774
|
+
}
|
|
775
|
+
const checks = [];
|
|
776
|
+
for (const id of tests) {
|
|
777
|
+
const mine = [];
|
|
778
|
+
const n = nodes[id];
|
|
779
|
+
const tree = n.evaluate ? evaluateTree(n.evaluate.subjects, n.evaluate.conds) : n.cond ? parseCondition(n.cond, resolve) : { k: 'opaque' };
|
|
780
|
+
n.tree = tree;
|
|
781
|
+
const whenTrue = consOf(tree, true);
|
|
782
|
+
const whenFalse = consOf(tree, false);
|
|
783
|
+
const restricts = tested(tree);
|
|
784
|
+
const keys = new Set([...whenTrue.keys(), ...whenFalse.keys(), ...restricts.keys()]);
|
|
785
|
+
n.gen = { true: [], false: [] };
|
|
786
|
+
const where = n.at || n.st;
|
|
787
|
+
for (const key of keys) {
|
|
788
|
+
const field = restricts.get(key) || (typeof key === 'string' ? { index: key.slice(3) } : key);
|
|
789
|
+
noteField(key, field);
|
|
790
|
+
const c = { id: checks.length, key, field, file: where.file, line: where.line, tCons: whenTrue.get(key) || null, fCons: whenFalse.get(key) || null, restricts: restricts.has(key) };
|
|
791
|
+
checks.push(c);
|
|
792
|
+
mine.push(c);
|
|
793
|
+
}
|
|
794
|
+
n.checks = mine;
|
|
795
|
+
}
|
|
796
|
+
// A MOVE of a constant, or a SET of a condition-name TO TRUE, leaves the field holding exactly that
|
|
797
|
+
// value until something writes it again: a fact like a check's outcome, made by a statement. It is
|
|
798
|
+
// what makes a length clamped to LENGTH OF a field a bound, and a field refilled with a literal
|
|
799
|
+
// before its use hold the literal rather than whatever reached it before. An element of a table is
|
|
800
|
+
// left out: the fact would be about one element, and the field stands for all of them.
|
|
801
|
+
const inTable = (it) => { for (let a = it; a; a = a.parent) if ((a.occurs || 1) > 1) return true; return false; };
|
|
802
|
+
const assigns = [];
|
|
803
|
+
for (const n of nodes) {
|
|
804
|
+
if (n.kind !== 'stmt' || !n.st || n.st.at == null) continue;
|
|
805
|
+
const st = n.st;
|
|
806
|
+
const seg = tokens.slice(st.at + 1, st.end);
|
|
807
|
+
let value = null;
|
|
808
|
+
let targets = [];
|
|
809
|
+
if (st.verb === 'MOVE') {
|
|
810
|
+
const to = seg.findIndex((t) => WORD(t, 'TO'));
|
|
811
|
+
if (to < 0) continue;
|
|
812
|
+
const src = seg.slice(0, to);
|
|
813
|
+
if (src.length === 3 && WORD(src[0], 'LENGTH') && WORD(src[1], 'OF')) {
|
|
814
|
+
const it = resolve(src[2]);
|
|
815
|
+
value = it && !it.index && it.size != null ? String(it.size) : null;
|
|
816
|
+
} else value = constValue(src);
|
|
817
|
+
targets = (st.targets || []).map((t) => resolve(t)).filter((it) => it && !it.index && it.level !== 88 && !inTable(it));
|
|
818
|
+
} else if (st.verb === 'SET' && seg.length >= 3 && WORD(seg[seg.length - 1], 'TRUE') && WORD(seg[seg.length - 2], 'TO')) {
|
|
819
|
+
for (const t of st.targets || []) {
|
|
820
|
+
const c = resolve(t);
|
|
821
|
+
if (!c || c.level !== 88 || !c.parent || inTable(c.parent)) continue;
|
|
822
|
+
const v = (c.values || [])[0];
|
|
823
|
+
const val = v && v.t === 'lit' ? String(v.v).trimEnd() : v && (v.t === 'num' || /^\d+$/.test(v.v)) ? String(Number(v.v)) : null;
|
|
824
|
+
if (val != null) assigns.push({ n, field: c.parent, value: val, tok: v });
|
|
825
|
+
}
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (value == null) continue;
|
|
829
|
+
const tok = seg.findIndex((t) => WORD(t, 'TO')) === 1 ? seg[0] : null;
|
|
830
|
+
for (const field of targets) assigns.push({ n, field, value, tok });
|
|
831
|
+
}
|
|
832
|
+
for (const a of assigns) {
|
|
833
|
+
const key = fieldKey(a.field);
|
|
834
|
+
noteField(key, a.field);
|
|
835
|
+
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
|
+
checks.push(a.c);
|
|
837
|
+
}
|
|
838
|
+
// Fact numbers: for each check, `x` that it ran (only a restricting one), `t` and `f` that it came
|
|
839
|
+
// out true or false with something to say about its field.
|
|
840
|
+
let nFacts = 0;
|
|
841
|
+
for (const c of checks) {
|
|
842
|
+
c.x = c.restricts ? nFacts++ : null;
|
|
843
|
+
c.t = c.tCons ? nFacts++ : null;
|
|
844
|
+
c.f = c.fCons ? nFacts++ : null;
|
|
845
|
+
}
|
|
846
|
+
for (const id of tests) {
|
|
847
|
+
const n = nodes[id];
|
|
848
|
+
for (const c of n.checks) {
|
|
849
|
+
if (c.x != null) { n.gen.true.push(c.x); n.gen.false.push(c.x); }
|
|
850
|
+
if (c.t != null) n.gen.true.push(c.t);
|
|
851
|
+
if (c.f != null) n.gen.false.push(c.f);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// A constant also makes true every outcome it satisfies of a check on the same field, so a length
|
|
855
|
+
// clamped to 80 on one branch and tested not above 80 on the other holds the test's outcome on
|
|
856
|
+
// both, and the join after them keeps it.
|
|
857
|
+
const checksOf = new Map();
|
|
858
|
+
for (const c of checks) if (!c.assigned) { if (!checksOf.has(c.key)) checksOf.set(c.key, []); checksOf.get(c.key).push(c); }
|
|
859
|
+
// Each assignment of a constant settles every check on its field, so the cost is their product
|
|
860
|
+
// field by field: a generated program that sets and tests the same counters tens of thousands of
|
|
861
|
+
// times each ran out of heap here.
|
|
862
|
+
let settled = 0;
|
|
863
|
+
for (const a of assigns) settled += (checksOf.get(a.c.key) || []).length;
|
|
864
|
+
if (settled > SETTLE_BUDGET) throw new Error(`${settled} check outcomes settled by assignment is past the ordering budget`);
|
|
865
|
+
for (const a of assigns) {
|
|
866
|
+
const gen = (a.n.genStmt ||= []);
|
|
867
|
+
gen.push(a.c.t);
|
|
868
|
+
for (const c of checksOf.get(a.c.key) || []) {
|
|
869
|
+
if (c.t != null && satisfies(c.tCons, a.value)) gen.push(c.t);
|
|
870
|
+
if (c.f != null && satisfies(c.fCons, a.value)) gen.push(c.f);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const assignsOf = new Map();
|
|
874
|
+
for (const a of assigns) { if (!assignsOf.has(a.n.st)) assignsOf.set(a.n.st, []); assignsOf.get(a.n.st).push(a); }
|
|
875
|
+
const nested = (el) => (el.k === 'if' ? [el.then, el.else] : el.k === 'evaluate' ? el.groups.map((g) => g.body)
|
|
876
|
+
: el.k === 'search' ? [...(el.atEnd ? [el.atEnd] : []), ...el.whens.map((w) => w.body)] : el.k === 'inline' ? [el.body] : el.k === 'phrased' ? el.bodies : []);
|
|
877
|
+
|
|
878
|
+
// A count INSPECT TALLYING adds to holds at most what it held plus the length inspected: each
|
|
879
|
+
// comparison cycle adds at most one and moves past at least one character. What it held is known
|
|
880
|
+
// where a MOVE of a constant reaches the INSPECT through the statements between and no other way.
|
|
881
|
+
const segOf = (st) => tokens.slice(st.at + 1, st.end);
|
|
882
|
+
const QUIET = new Set(['MOVE', 'SET', 'COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'INITIALIZE', 'INITIALISE', 'DISPLAY', 'CONTINUE', 'STRING', 'ACCEPT', 'INSPECT']);
|
|
883
|
+
const LENGTH_KEEPING = new Set(['REVERSE', 'UPPER-CASE', 'LOWER-CASE', 'TRIM']);
|
|
884
|
+
const inspectedSize = (op) => {
|
|
885
|
+
if (WORD(op[0], 'FUNCTION') && op[1] && LENGTH_KEEPING.has(op[1].u) && op[2] && op[2].v === '(' && op[op.length - 1].v === ')') {
|
|
886
|
+
op = op.slice(3, -1).filter((t) => !WORD(t, 'LEADING') && !WORD(t, 'TRAILING'));
|
|
887
|
+
}
|
|
888
|
+
// An identifier, its qualifiers and a subscript: an element is no longer than its item. A
|
|
889
|
+
// reference modification can be.
|
|
890
|
+
let i = 1;
|
|
891
|
+
while (i + 1 < op.length && (WORD(op[i], 'OF') || WORD(op[i], 'IN')) && op[i + 1].t === 'word') i += 2;
|
|
892
|
+
if (i < op.length) {
|
|
893
|
+
if (op[i].v !== '(' || op[op.length - 1].v !== ')' || op.slice(i).some((t) => t.t === 'op' && t.v === ':')) return null;
|
|
894
|
+
}
|
|
895
|
+
const it = op[0] && op[0].t === 'word' ? resolve(op[0]) : null;
|
|
896
|
+
return it && !it.index && it.level !== 88 && it.size != null ? it.size : null;
|
|
897
|
+
};
|
|
898
|
+
const writesRecordOf = (st, item) => (st.targets || []).some((t) => { const it = resolve(t); return it && !it.index && itemsInRecord(it) === itemsInRecord(item); });
|
|
899
|
+
const tallyIn = (seq) => {
|
|
900
|
+
for (let j = 0; j < seq.length; j++) {
|
|
901
|
+
const el = seq[j];
|
|
902
|
+
if (!el || el.k !== 'stmt' || el.st.verb !== 'INSPECT' || !el.st.counts) continue;
|
|
903
|
+
const seg = segOf(el.st);
|
|
904
|
+
const at = seg.findIndex((t) => WORD(t, 'TALLYING'));
|
|
905
|
+
const count = seg[at + 1] ? resolve(seg[at + 1]) : null;
|
|
906
|
+
const size = inspectedSize(seg.slice(0, at));
|
|
907
|
+
if (!count || count.index || count.level === 88 || size == null) continue;
|
|
908
|
+
// The count holds what the tally adds without losing a digit.
|
|
909
|
+
const room = 10 ** String(count.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n))).split(/[V.]/)[0].replace(/[^9]/g, '').length - 1;
|
|
910
|
+
for (let i = j - 1; i >= 0; i--) {
|
|
911
|
+
const prev = seq[i];
|
|
912
|
+
if (!prev || prev.k !== 'stmt' || !QUIET.has(prev.st.verb)) break;
|
|
913
|
+
const a = (assignsOf.get(prev.st) || []).find((x) => x.field === count);
|
|
914
|
+
const others = (prev.st.targets || []).some((t) => { const it = resolve(t); return it && it !== count && !it.index && itemsInRecord(it) === itemsInRecord(count); });
|
|
915
|
+
if (a && !others) {
|
|
916
|
+
const n = nodes[nodeOf.get(el.st)];
|
|
917
|
+
const start = numberOf(a.value);
|
|
918
|
+
if (start != null && Number.isInteger(start) && start >= 0 && start + size <= room && n) {
|
|
919
|
+
const key = fieldKey(count);
|
|
920
|
+
noteField(key, count);
|
|
921
|
+
const bounds = { lo: start, hi: start + size };
|
|
922
|
+
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
|
+
checks.push(c);
|
|
924
|
+
(n.genStmt ||= []).push(c.t);
|
|
925
|
+
}
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
if ((prev.st.verb === 'INSPECT' && segOf(prev.st).some((t) => WORD(t, 'TALLYING'))) || writesRecordOf(prev.st, count)) break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
// A paragraph's sentences run in order, and one is entered from anywhere but the one before it
|
|
933
|
+
// only by a NEXT SENTENCE in that one: there the run is cut.
|
|
934
|
+
const nextSentenceIn = (list) => list.some((el) => el.k === 'next-sentence' || nested(el).some(nextSentenceIn));
|
|
935
|
+
const eachList = (list) => { tallyIn(list); for (const el of list) for (const l of nested(el)) eachList(l); };
|
|
936
|
+
for (const p of paras) {
|
|
937
|
+
const run = [];
|
|
938
|
+
p.sentences.forEach((s, i) => {
|
|
939
|
+
if (i && nextSentenceIn(p.sentences[i - 1])) run.push(null);
|
|
940
|
+
run.push(...s);
|
|
941
|
+
for (const el of s) for (const l of nested(el)) eachList(l);
|
|
942
|
+
});
|
|
943
|
+
tallyIn(run);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// A test whose failing branch sets a flag leaves "the flag holds one of the values that branch
|
|
947
|
+
// gives it, or the field holds what the test's other outcome said". A later test ruling out those
|
|
948
|
+
// values leaves the second. Each such fact dies with a write to either field, and what it yields
|
|
949
|
+
// dies with a write to the second, so a range's summary still composes.
|
|
950
|
+
// A flag's values are compared only where nothing but the literal decides the comparison: an
|
|
951
|
+
// alphanumeric literal against an alphanumeric item, which a value it is given must fill so that
|
|
952
|
+
// 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
|
+
const plainLiteral = (tok) => tok && tok.t === 'lit' && !tok.prefix;
|
|
967
|
+
const integerOf = (tok) => (tok && (tok.t === 'num' || tok.t === 'word') && /^\d+$/.test(tok.v) ? tok.v : null);
|
|
968
|
+
const heldAs = (field, tok) => {
|
|
969
|
+
const cat = categoryOf(field);
|
|
970
|
+
if (!cat) return null;
|
|
971
|
+
if (cat.k === 'A') return plainLiteral(tok) && String(tok.v).length === field.size && String(tok.v).trim() ? `A:${String(tok.v).trimEnd()}` : null;
|
|
972
|
+
const digits = integerOf(tok);
|
|
973
|
+
return digits != null && digits.replace(/^0+(?=\d)/, '').length <= cat.digits ? `N:${Number(digits)}` : null;
|
|
974
|
+
};
|
|
975
|
+
const comparedAs = (field, tok) => {
|
|
976
|
+
const cat = categoryOf(field);
|
|
977
|
+
if (!cat) return null;
|
|
978
|
+
if (cat.k === 'A') return plainLiteral(tok) ? `A:${String(tok.v).trimEnd()}` : null;
|
|
979
|
+
const digits = integerOf(tok);
|
|
980
|
+
return digits != null ? `N:${Number(digits)}` : null;
|
|
981
|
+
};
|
|
982
|
+
const heldBy = new Map();
|
|
983
|
+
const held = (a) => { if (!heldBy.has(a)) heldBy.set(a, heldAs(a.field, a.tok)); return heldBy.get(a); };
|
|
984
|
+
const flagFields = (tree, out = new Set()) => {
|
|
985
|
+
if (tree.k === 'not') flagFields(tree.a, out);
|
|
986
|
+
else if (tree.k === 'and' || tree.k === 'or') { flagFields(tree.a, out); flagFields(tree.b, out); }
|
|
987
|
+
else if (tree.k === 'rel') { for (const side of [tree.left, tree.right]) { const f = fieldOf(side); if (f) out.add(f); } }
|
|
988
|
+
else if (tree.k === 'cond' && tree.subject.length === 1) { const c = resolve(tree.subject[0]); if (c && c.level === 88 && c.parent) out.add(c.parent); }
|
|
989
|
+
return out;
|
|
990
|
+
};
|
|
991
|
+
// Whether an outcome of a condition rules out that `field` holds `value`.
|
|
992
|
+
function excludes(tree, outcome, field, value) {
|
|
993
|
+
switch (tree.k) {
|
|
994
|
+
case 'not': return excludes(tree.a, !outcome, field, value);
|
|
995
|
+
case 'and': return outcome ? excludes(tree.a, true, field, value) || excludes(tree.b, true, field, value)
|
|
996
|
+
: excludes(tree.a, false, field, value) && excludes(tree.b, false, field, value);
|
|
997
|
+
case 'or': return outcome ? excludes(tree.a, true, field, value) && excludes(tree.b, true, field, value)
|
|
998
|
+
: excludes(tree.a, false, field, value) || excludes(tree.b, false, field, value);
|
|
999
|
+
case 'rel': {
|
|
1000
|
+
const other = fieldOf(tree.left) === field ? tree.right : fieldOf(tree.right) === field ? tree.left : null;
|
|
1001
|
+
const v = other && other.length === 1 ? comparedAs(field, other[0]) : null;
|
|
1002
|
+
if (v == null) return false;
|
|
1003
|
+
const op = outcome ? tree.op : NEGATE[tree.op];
|
|
1004
|
+
return op === '=' ? v !== value : op === '<>' ? v === value : false;
|
|
1005
|
+
}
|
|
1006
|
+
case 'cond': {
|
|
1007
|
+
const c = tree.subject.length === 1 ? resolve(tree.subject[0]) : null;
|
|
1008
|
+
if (!c || c.level !== 88 || c.parent !== field || !(c.values || []).length) return false;
|
|
1009
|
+
const vals = c.values.map((t) => (WORD(t, 'THRU') || WORD(t, 'THROUGH') ? null : comparedAs(field, t)));
|
|
1010
|
+
if (vals.includes(null)) return false;
|
|
1011
|
+
const named = vals.includes(value);
|
|
1012
|
+
return outcome ? !named : named;
|
|
1013
|
+
}
|
|
1014
|
+
default: return false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const stmtsIn = (list, out = []) => { for (const el of list) { if (el.k === 'stmt') out.push(el.st); for (const l of nested(el)) stmtsIn(l, out); } return out; };
|
|
1018
|
+
const flags = [];
|
|
1019
|
+
const implied = new Map();
|
|
1020
|
+
for (const id of tests) {
|
|
1021
|
+
const n = nodes[id];
|
|
1022
|
+
if (!n.branches) continue;
|
|
1023
|
+
for (const outcome of [true, false]) {
|
|
1024
|
+
const said = n.checks.filter((c) => (outcome ? c.tCons : c.fCons));
|
|
1025
|
+
if (!said.length) continue;
|
|
1026
|
+
const valuesOf = new Map();
|
|
1027
|
+
const failing = n.branches[outcome ? 1 : 0];
|
|
1028
|
+
for (const st of stmtsIn(failing.later ? failing.later.slice(failing.from).flatMap((g) => g.body) : failing)) {
|
|
1029
|
+
for (const a of assignsOf.get(st) || []) {
|
|
1030
|
+
const v = held(a);
|
|
1031
|
+
if (v == null) continue;
|
|
1032
|
+
if (!valuesOf.has(a.field)) valuesOf.set(a.field, new Set());
|
|
1033
|
+
valuesOf.get(a.field).add(v);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
for (const [field, values] of valuesOf) {
|
|
1037
|
+
const fKey = fieldKey(field);
|
|
1038
|
+
noteField(fKey, field);
|
|
1039
|
+
for (const c of said) {
|
|
1040
|
+
if (c.key === fKey) continue;
|
|
1041
|
+
let yields = implied.get(`${c.id}|${outcome}`);
|
|
1042
|
+
if (!yields) {
|
|
1043
|
+
yields = { id: checks.length, key: c.key, field: c.field, file: c.file, line: c.line, tCons: outcome ? c.tCons : c.fCons, fCons: null, restricts: false, x: null, t: nFacts++, f: null };
|
|
1044
|
+
checks.push(yields);
|
|
1045
|
+
implied.set(`${c.id}|${outcome}`, yields);
|
|
1046
|
+
}
|
|
1047
|
+
const bit = nFacts++;
|
|
1048
|
+
checks.push({ pseudo: true, key: fKey, field, x: null, t: bit, f: null }, { pseudo: true, key: c.key, field: c.field, x: null, t: bit, f: null });
|
|
1049
|
+
n.gen[String(outcome)].push(bit);
|
|
1050
|
+
flags.push({ field, values: [...values], bit, yields: yields.t });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (flags.length) {
|
|
1056
|
+
const byField = new Map();
|
|
1057
|
+
for (const fl of flags) { if (!byField.has(fl.field)) byField.set(fl.field, []); byField.get(fl.field).push(fl); }
|
|
1058
|
+
for (const a of assigns) {
|
|
1059
|
+
const fls = byField.get(a.field);
|
|
1060
|
+
const v = fls && held(a);
|
|
1061
|
+
if (v != null) for (const fl of fls) if (fl.values.includes(v)) (a.n.genStmt ||= []).push(fl.bit);
|
|
1062
|
+
}
|
|
1063
|
+
const outcomes = new Map();
|
|
1064
|
+
for (const node of nodes) if (node.kind === 'outcome') outcomes.set(`${node.test}|${node.value}`, node);
|
|
1065
|
+
for (const id of tests) {
|
|
1066
|
+
const tree = nodes[id].tree;
|
|
1067
|
+
for (const field of flagFields(tree)) {
|
|
1068
|
+
for (const fl of byField.get(field) || []) {
|
|
1069
|
+
for (const outcome of [true, false]) {
|
|
1070
|
+
const at = outcomes.get(`${id}|${outcome}`);
|
|
1071
|
+
if (at && fl.values.every((v) => excludes(tree, outcome, field, v))) (at.implies ||= []).push([fl.bit, fl.yields]);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// At least one fact, so the analysis runs, and a statement it never reaches has no facts at all.
|
|
1078
|
+
nFacts ||= 1;
|
|
1079
|
+
const extraFacts = new Map();
|
|
1080
|
+
if (extra) {
|
|
1081
|
+
for (const id of tests) {
|
|
1082
|
+
const n = nodes[id];
|
|
1083
|
+
const said = extra({ cond: n.cond || null, evaluate: n.evaluate || null, st: n.st, at: n.at || n.st });
|
|
1084
|
+
for (const outcome of ['true', 'false']) {
|
|
1085
|
+
const key = said && said[outcome];
|
|
1086
|
+
if (!key) continue;
|
|
1087
|
+
if (!extraFacts.has(key)) extraFacts.set(key, nFacts++);
|
|
1088
|
+
n.gen[outcome].push(extraFacts.get(key));
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (!nFacts) return { nodeOf, checks: [], words: 0, facts: new Map(), nodes: nodes.length, extraFacts };
|
|
1093
|
+
const words = Math.ceil(nFacts / 32);
|
|
1094
|
+
// Every node holds a bitset of every fact, several times over, so the cost is their product. A
|
|
1095
|
+
// 426,000-line generated program passed an 8 GB heap here, and running out of heap cannot be
|
|
1096
|
+
// caught. Past the budget the program is left unordered, which callers already handle.
|
|
1097
|
+
if (nodes.length * words * 4 > FACT_BUDGET_BYTES) throw new Error(`${nodes.length} statements by ${nFacts} facts is past the ordering budget`);
|
|
1098
|
+
|
|
1099
|
+
// What a write kills: every fact about a field whose bytes it overlaps.
|
|
1100
|
+
const factsOfKey = new Map();
|
|
1101
|
+
for (const c of checks) {
|
|
1102
|
+
const list = factsOfKey.get(c.key) || [];
|
|
1103
|
+
for (const f of [c.x, c.t, c.f]) if (f != null) list.push(f);
|
|
1104
|
+
factsOfKey.set(c.key, list);
|
|
1105
|
+
}
|
|
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
|
+
const killCache = new Map();
|
|
1117
|
+
const killOf = (w) => {
|
|
1118
|
+
if (!w) return null;
|
|
1119
|
+
let bits = killCache.get(w);
|
|
1120
|
+
if (bits !== undefined) return bits;
|
|
1121
|
+
bits = null;
|
|
1122
|
+
for (const [key, list] of factsOfKey) {
|
|
1123
|
+
const f = fields.get(key);
|
|
1124
|
+
if (!f || !overlaps(w, f)) continue;
|
|
1125
|
+
bits ||= new Uint32Array(words);
|
|
1126
|
+
for (const x of list) bits[x >>> 5] |= 1 << (x & 31);
|
|
1127
|
+
}
|
|
1128
|
+
killCache.set(w, bits);
|
|
1129
|
+
return bits;
|
|
1130
|
+
};
|
|
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
|
+
for (const n of nodes) {
|
|
1170
|
+
if (n.kind !== 'stmt' && n.kind !== 'exec' && n.kind !== 'step' && n.kind !== 'call') continue;
|
|
1171
|
+
let bits = null;
|
|
1172
|
+
for (const w of writtenBy(n)) {
|
|
1173
|
+
const k2 = killOf(w);
|
|
1174
|
+
if (!k2) continue;
|
|
1175
|
+
bits ||= new Uint32Array(words);
|
|
1176
|
+
for (let i = 0; i < words; i++) bits[i] |= k2[i];
|
|
1177
|
+
}
|
|
1178
|
+
if (bits) n.kill = bits;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// ---- must-analysis ----
|
|
1182
|
+
const ZERO = new Uint32Array(words);
|
|
1183
|
+
const ONES = new Uint32Array(words).fill(0xffffffff);
|
|
1184
|
+
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);
|
|
1187
|
+
const summaries = new Map();
|
|
1188
|
+
// Reverse postorder from every way in, so a forward analysis visits a node after what precedes it
|
|
1189
|
+
// and a join is revisited only when a loop brings something new.
|
|
1190
|
+
const order = new Int32Array(nodes.length).fill(-1);
|
|
1191
|
+
{
|
|
1192
|
+
const seen = new Uint8Array(nodes.length);
|
|
1193
|
+
const post = [];
|
|
1194
|
+
const roots = [mainPara.entry, ...[...extraEntries].map((n) => entryOf.get(n)).filter((x) => x != null), ...entryStatements,
|
|
1195
|
+
...[...ranges.values()].map((r) => r.entry).filter((x) => x != null), ...paras.map((p) => p.entry)];
|
|
1196
|
+
for (const root of roots) {
|
|
1197
|
+
if (seen[root]) continue;
|
|
1198
|
+
const stack = [[root, 0]];
|
|
1199
|
+
seen[root] = 1;
|
|
1200
|
+
while (stack.length) {
|
|
1201
|
+
const top = stack[stack.length - 1];
|
|
1202
|
+
const succ = nodes[top[0]].succ;
|
|
1203
|
+
if (top[1] < succ.length) {
|
|
1204
|
+
const s = succ[top[1]++];
|
|
1205
|
+
if (!seen[s]) { seen[s] = 1; stack.push([s, 0]); }
|
|
1206
|
+
} else { post.push(top[0]); stack.pop(); }
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
for (let i = 0; i < post.length; i++) order[post[i]] = post.length - 1 - i;
|
|
1210
|
+
}
|
|
1211
|
+
const scratch = new Uint32Array(words);
|
|
1212
|
+
// What holds after a node, in a buffer reused by every call: the caller merges it into the next
|
|
1213
|
+
// node's facts before calling again. Null where the route ends here.
|
|
1214
|
+
const transfer = (n, input) => {
|
|
1215
|
+
let out = input;
|
|
1216
|
+
if (n.kind === 'outcome') {
|
|
1217
|
+
const g = n.value ? nodes[n.test].genT : nodes[n.test].genF;
|
|
1218
|
+
if (g) { for (let i = 0; i < words; i++) scratch[i] = input[i] | g[i]; out = scratch; }
|
|
1219
|
+
} else if (n.kind === 'call') {
|
|
1220
|
+
// A paragraph nobody can find may write anything. A range still being summarised has not
|
|
1221
|
+
// returned yet, which is the optimistic start a must-analysis iterates down from.
|
|
1222
|
+
if (n.range.entry == null || n.range.end == null) { scratch.fill(0); return scratch; }
|
|
1223
|
+
const s = summaries.get(n.range.key);
|
|
1224
|
+
if (!s || !s.top) return null;
|
|
1225
|
+
for (let i = 0; i < words; i++) scratch[i] = (input[i] & s.top[i]) | s.bot[i];
|
|
1226
|
+
out = scratch;
|
|
1227
|
+
}
|
|
1228
|
+
if (n.kill) {
|
|
1229
|
+
if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] & ~n.kill[i]; out = scratch; }
|
|
1230
|
+
else for (let i = 0; i < words; i++) out[i] &= ~n.kill[i];
|
|
1231
|
+
}
|
|
1232
|
+
// What the statement assigns holds after it, over whatever the write took away.
|
|
1233
|
+
if (n.genS) {
|
|
1234
|
+
if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] | n.genS[i]; out = scratch; }
|
|
1235
|
+
else for (let i = 0; i < words; i++) out[i] |= n.genS[i];
|
|
1236
|
+
}
|
|
1237
|
+
// A flag test's outcome turns what held before it into the bound the flag stood for.
|
|
1238
|
+
if (n.implies) {
|
|
1239
|
+
for (const [from, to] of n.implies) {
|
|
1240
|
+
if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
|
|
1241
|
+
if (out === input) { scratch.set(input); out = scratch; }
|
|
1242
|
+
out[to >>> 5] |= 1 << (to & 31);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return out;
|
|
1246
|
+
};
|
|
1247
|
+
const same = (a, b) => { for (let i = 0; i < words; i++) if (a[i] !== b[i]) return false; return true; };
|
|
1248
|
+
const and = (a, b) => { const o = new Uint32Array(words); for (let i = 0; i < words; i++) o[i] = a[i] & b[i]; return o; };
|
|
1249
|
+
// Joins `out` into `into`, in place; true if anything was taken away.
|
|
1250
|
+
const meetInto = (into, out) => {
|
|
1251
|
+
let changed = false;
|
|
1252
|
+
// >>> 0: a bitwise AND is signed, the store is not, and bit 31 would read as always changed.
|
|
1253
|
+
for (let i = 0; i < words; i++) { const v = (into[i] & out[i]) >>> 0; if (v !== into[i]) { into[i] = v; changed = true; } }
|
|
1254
|
+
return changed;
|
|
1255
|
+
};
|
|
1256
|
+
// A binary heap of node ids by reverse postorder.
|
|
1257
|
+
const heap = [];
|
|
1258
|
+
const inHeap = new Uint8Array(nodes.length);
|
|
1259
|
+
const push = (id) => {
|
|
1260
|
+
if (inHeap[id]) return;
|
|
1261
|
+
inHeap[id] = 1;
|
|
1262
|
+
heap.push(id);
|
|
1263
|
+
let i = heap.length - 1;
|
|
1264
|
+
while (i > 0) { const p = (i - 1) >> 1; if (order[heap[p]] <= order[heap[i]]) break; [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; }
|
|
1265
|
+
};
|
|
1266
|
+
const pop = () => {
|
|
1267
|
+
const top = heap[0];
|
|
1268
|
+
const last = heap.pop();
|
|
1269
|
+
if (heap.length) {
|
|
1270
|
+
heap[0] = last;
|
|
1271
|
+
let i = 0;
|
|
1272
|
+
for (;;) {
|
|
1273
|
+
const l = 2 * i + 1, r = l + 1;
|
|
1274
|
+
let m = i;
|
|
1275
|
+
if (l < heap.length && order[heap[l]] < order[heap[m]]) m = l;
|
|
1276
|
+
if (r < heap.length && order[heap[r]] < order[heap[m]]) m = r;
|
|
1277
|
+
if (m === i) break;
|
|
1278
|
+
[heap[m], heap[i]] = [heap[i], heap[m]];
|
|
1279
|
+
i = m;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
inHeap[top] = 0;
|
|
1283
|
+
return top;
|
|
1284
|
+
};
|
|
1285
|
+
// Every route from `entry` that reaches `stopAt` or the program's end, meeting at joins. A call
|
|
1286
|
+
// whose range never returns ends the route, as a STOP RUN does. `calls` collects the ranges the
|
|
1287
|
+
// routes perform, for whoever needs to know what depends on what.
|
|
1288
|
+
function run(entry, input, stopAt, calls = null) {
|
|
1289
|
+
const IN = new Map();
|
|
1290
|
+
let exitOut = null;
|
|
1291
|
+
IN.set(entry, input.slice());
|
|
1292
|
+
push(entry);
|
|
1293
|
+
while (heap.length) {
|
|
1294
|
+
const id = pop();
|
|
1295
|
+
const n = nodes[id];
|
|
1296
|
+
if (calls && n.kind === 'call' && n.range.entry != null) calls.add(n.range.key);
|
|
1297
|
+
const out = transfer(n, IN.get(id));
|
|
1298
|
+
if (!out) continue;
|
|
1299
|
+
if (id === stopAt) { if (exitOut) meetInto(exitOut, out); else exitOut = out.slice(); continue; }
|
|
1300
|
+
for (const s of n.succ) {
|
|
1301
|
+
const cur = IN.get(s);
|
|
1302
|
+
if (!cur) { IN.set(s, out.slice()); push(s); } else if (meetInto(cur, out)) push(s);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return { IN, exitOut };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Summaries: what holds after a range, from all facts and from none. A range is summarised again
|
|
1309
|
+
// only when a range it performs changes. One still being computed has not returned, which is what
|
|
1310
|
+
// a recursive PERFORM does anyway.
|
|
1311
|
+
const performed = [...ranges.values()].filter((r) => r.entry != null && r.end != null);
|
|
1312
|
+
const callers = new Map();
|
|
1313
|
+
const pending = [...performed].sort((a, b) => order[b.entry] - order[a.entry]);
|
|
1314
|
+
const queued = new Set(pending.map((r) => r.key));
|
|
1315
|
+
for (let steps = 0; pending.length && steps < performed.length * 20; steps++) {
|
|
1316
|
+
const r = pending.shift();
|
|
1317
|
+
queued.delete(r.key);
|
|
1318
|
+
const calls = new Set();
|
|
1319
|
+
const top = run(r.entry, ONES, r.end, calls).exitOut;
|
|
1320
|
+
const bot = run(r.entry, ZERO, r.end).exitOut;
|
|
1321
|
+
for (const k of calls) { if (!callers.has(k)) callers.set(k, new Set()); callers.get(k).add(r); }
|
|
1322
|
+
const old = summaries.get(r.key);
|
|
1323
|
+
const nextS = top && bot ? { top, bot } : { top: null, bot: null };
|
|
1324
|
+
const moved = !old || (old.top === null) !== (nextS.top === null) || (nextS.top && (!same(old.top, nextS.top) || !same(old.bot, nextS.bot)));
|
|
1325
|
+
if (!moved) continue;
|
|
1326
|
+
summaries.set(r.key, nextS);
|
|
1327
|
+
for (const c of callers.get(r.key) || []) if (!queued.has(c.key)) { queued.add(c.key); pending.push(c); }
|
|
1328
|
+
}
|
|
1329
|
+
// Stopped at a step limit, a summary may say a range never returns when it does.
|
|
1330
|
+
let partial = pending.length > 0;
|
|
1331
|
+
|
|
1332
|
+
// Contexts: the program's own entry and every extra one enter with nothing; a range enters with
|
|
1333
|
+
// what holds at every PERFORM of it that some context reaches. A context runs again only when what
|
|
1334
|
+
// enters it shrinks.
|
|
1335
|
+
const results = new Map();
|
|
1336
|
+
const rangeIn = new Map();
|
|
1337
|
+
const byKey = new Map(performed.map((r) => [r.key, r]));
|
|
1338
|
+
const work = [{ key: `main:${mainPara.entry}`, entry: mainPara.entry, stopAt: null }];
|
|
1339
|
+
for (const name of extraEntries) if (entryOf.has(name)) work.push({ key: `main:${entryOf.get(name)}`, entry: entryOf.get(name), stopAt: null });
|
|
1340
|
+
for (const id of entryStatements) work.push({ key: `main:${id}`, entry: id, stopAt: null });
|
|
1341
|
+
const waiting = new Set(work.map((c) => c.key));
|
|
1342
|
+
for (let steps = 0; work.length && steps < (performed.length + work.length) * 50; steps++) {
|
|
1343
|
+
const c = work.shift();
|
|
1344
|
+
waiting.delete(c.key);
|
|
1345
|
+
const input = c.stopAt == null ? ZERO : rangeIn.get(c.key);
|
|
1346
|
+
const res = run(c.entry, input, c.stopAt);
|
|
1347
|
+
results.set(c.key, res.IN);
|
|
1348
|
+
for (const [id, bits] of res.IN) {
|
|
1349
|
+
const n = nodes[id];
|
|
1350
|
+
if (n.kind !== 'call' || !byKey.has(n.range.key)) continue;
|
|
1351
|
+
const had = rangeIn.get(n.range.key);
|
|
1352
|
+
let moved = false;
|
|
1353
|
+
if (!had) { rangeIn.set(n.range.key, bits.slice()); moved = true; } else moved = meetInto(had, bits);
|
|
1354
|
+
if (moved && !waiting.has(n.range.key)) {
|
|
1355
|
+
const r = byKey.get(n.range.key);
|
|
1356
|
+
waiting.add(r.key);
|
|
1357
|
+
work.push({ key: r.key, entry: r.entry, stopAt: r.end });
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (work.length || unknownLabel) partial = true;
|
|
1362
|
+
const facts = new Map();
|
|
1363
|
+
for (const IN of results.values()) {
|
|
1364
|
+
for (const [id, bits] of IN) {
|
|
1365
|
+
const had = facts.get(id);
|
|
1366
|
+
if (had) meetInto(had, bits); else facts.set(id, bits.slice());
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// What a run can reach, from the graph alone and with the contexts the facts use: code entered from
|
|
1371
|
+
// an entry runs on past the end of a paragraph, and a performed range stops at its end. A PERFORM
|
|
1372
|
+
// returns once anything reaches the end of its range, a handler's label inside it included, and
|
|
1373
|
+
// an EXIT PROGRAM with no CALL active carries on. Whatever the facts say, this is at least as much.
|
|
1374
|
+
const reached = new Uint8Array(nodes.length);
|
|
1375
|
+
const seenIn = new Map();
|
|
1376
|
+
const waitingOn = new Map();
|
|
1377
|
+
const todo = [];
|
|
1378
|
+
const toVisit = (ctx, id) => { if (id != null) todo.push(ctx, id); };
|
|
1379
|
+
toVisit(null, mainPara.entry);
|
|
1380
|
+
for (const name of extraEntries) toVisit(null, entryOf.get(name));
|
|
1381
|
+
for (const id of entryStatements) toVisit(null, id);
|
|
1382
|
+
while (todo.length) {
|
|
1383
|
+
const id = todo.pop();
|
|
1384
|
+
const ctx = todo.pop();
|
|
1385
|
+
let seen = seenIn.get(ctx);
|
|
1386
|
+
if (!seen) { seen = new Set(); seenIn.set(ctx, seen); }
|
|
1387
|
+
if (seen.has(id)) continue;
|
|
1388
|
+
seen.add(id);
|
|
1389
|
+
if (!reached[id]) { reached[id] = 1; for (const [c, s] of waitingOn.get(id) || []) toVisit(c, s); }
|
|
1390
|
+
if (ctx && id === ctx.end) continue;
|
|
1391
|
+
const n = nodes[id];
|
|
1392
|
+
if (n.kind === 'call' && n.range.entry != null && n.range.end != null) {
|
|
1393
|
+
const r = n.range;
|
|
1394
|
+
toVisit(r, r.entry);
|
|
1395
|
+
for (const s of n.succ) {
|
|
1396
|
+
if (reached[r.end]) toVisit(ctx, s);
|
|
1397
|
+
else { if (!waitingOn.has(r.end)) waitingOn.set(r.end, []); waitingOn.get(r.end).push([ctx, s]); }
|
|
1398
|
+
}
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
for (const s of n.succ) toVisit(ctx, s);
|
|
1402
|
+
if (n.cont != null) toVisit(ctx, n.cont);
|
|
1403
|
+
}
|
|
1404
|
+
// Nothing known to have facts may be called unreached.
|
|
1405
|
+
for (const id of facts.keys()) if (id < nodes.length && !reached[id]) partial = true;
|
|
1406
|
+
if (altered) partial = true;
|
|
1407
|
+
|
|
1408
|
+
// A name read inside a condition is read with what the operands evaluated before it said. Each
|
|
1409
|
+
// level of a condition is evaluated left to right and stops once its value is known, so a name
|
|
1410
|
+
// behind an AND is read only if the left side was true, behind an OR only if it was false. A name
|
|
1411
|
+
// several relations read is judged by the weakest. Each such place is a point of its own, past the
|
|
1412
|
+
// end of the graph, holding the test's facts and a few more numbered past the analysis's.
|
|
1413
|
+
const posOf = new Map();
|
|
1414
|
+
const places = [];
|
|
1415
|
+
let placeFacts = nFacts;
|
|
1416
|
+
for (const id of tests) {
|
|
1417
|
+
const n = nodes[id];
|
|
1418
|
+
if (!n.st || !n.st.indexes || !n.tree || n.evaluate || !(n.st.verb === 'IF' || n.loop)) continue;
|
|
1419
|
+
const until = n.loop ? new Set(n.cond) : null;
|
|
1420
|
+
const reads = readersOf(n.tree);
|
|
1421
|
+
for (const x of n.st.indexes) {
|
|
1422
|
+
if (until && !until.has(x.tok)) continue;
|
|
1423
|
+
const guards = reads.get(x.tok);
|
|
1424
|
+
let said = null;
|
|
1425
|
+
let ran = null;
|
|
1426
|
+
for (const g of guards || []) {
|
|
1427
|
+
let cons = new Map();
|
|
1428
|
+
const r = new Map();
|
|
1429
|
+
for (const [sub, outcome] of g) { cons = meetMaps(cons, consOf(sub, outcome)); testedWhen(sub, outcome, r); }
|
|
1430
|
+
said = said ? joinMaps(said, cons) : cons;
|
|
1431
|
+
ran = ran ? new Map([...ran].filter(([k]) => r.has(k))) : r;
|
|
1432
|
+
}
|
|
1433
|
+
if (!said || (!said.size && !ran.size)) { if (until) posOf.set(x.tok, id); continue; }
|
|
1434
|
+
const mine = [];
|
|
1435
|
+
for (const key of new Set([...said.keys(), ...ran.keys()])) {
|
|
1436
|
+
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) || null, fCons: null, restricts: ran.has(key), f: null };
|
|
1438
|
+
c.x = c.restricts ? placeFacts++ : null;
|
|
1439
|
+
c.t = c.tCons ? placeFacts++ : null;
|
|
1440
|
+
checks.push(c);
|
|
1441
|
+
mine.push(c);
|
|
1442
|
+
}
|
|
1443
|
+
posOf.set(x.tok, nodes.length + places.length);
|
|
1444
|
+
places.push({ test: id, checks: mine });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const reachedAt = new Uint8Array(nodes.length + places.length);
|
|
1448
|
+
reachedAt.set(reached);
|
|
1449
|
+
places.forEach((p, i) => {
|
|
1450
|
+
reachedAt[nodes.length + i] = reached[p.test];
|
|
1451
|
+
const before = facts.get(p.test);
|
|
1452
|
+
if (!before) return;
|
|
1453
|
+
const bits = new Uint32Array(Math.max(words, Math.ceil(placeFacts / 32)));
|
|
1454
|
+
bits.set(before);
|
|
1455
|
+
for (const c of p.checks) for (const x of [c.x, c.t]) if (x != null) bits[x >>> 5] |= 1 << (x & 31);
|
|
1456
|
+
facts.set(nodes.length + i, bits);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// Plain data, no closures: a closure over this scope would keep the program's tokens alive after
|
|
1460
|
+
// the caller has let the parse tree go. A statement no context reaches has no facts, and credits
|
|
1461
|
+
// nothing. One `reached` does not mark does not run, unless `partial` says the analysis stopped
|
|
1462
|
+
// 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 };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// For each token a condition reads, what each reading of it waits on: the operands to its left at
|
|
1467
|
+
// every level, each with the outcome that lets evaluation go on.
|
|
1468
|
+
function readersOf(tree) {
|
|
1469
|
+
const out = new Map();
|
|
1470
|
+
const note = (toks, guard) => { for (const t of toks) { const l = out.get(t); if (l) l.push(guard); else out.set(t, [guard]); } };
|
|
1471
|
+
const walk = (t, guard) => {
|
|
1472
|
+
switch (t.k) {
|
|
1473
|
+
case 'not': walk(t.a, guard); break;
|
|
1474
|
+
case 'and': walk(t.a, guard); walk(t.b, [...guard, [t.a, true]]); break;
|
|
1475
|
+
case 'or': walk(t.a, guard); walk(t.b, [...guard, [t.a, false]]); break;
|
|
1476
|
+
case 'rel': note(t.left, guard); note(t.right, guard); break;
|
|
1477
|
+
case 'class': case 'sign': case 'cond': note(t.subject, guard); break;
|
|
1478
|
+
default: break;
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
walk(tree, []);
|
|
1482
|
+
return out;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export const hasFact = (bits, x) => x != null && bits != null && (bits[x >>> 5] & (1 << (x & 31))) !== 0;
|
|
1486
|
+
|
|
1487
|
+
// What a node's checks say at a point, given the facts that hold there: 2 when the value it holds is
|
|
1488
|
+
// safe for the sink, 1 when a check has run on every route to the point, 0 when neither. The check
|
|
1489
|
+
// named is the one that decided it: one whose outcome alone makes the value safe, if there is one.
|
|
1490
|
+
// A bound the program's data sets rather than a check it made counts only where it keeps the index
|
|
1491
|
+
// between 1 and the sink's `limit`.
|
|
1492
|
+
export function creditOf(checks, bits, sinkKind, limit = null) {
|
|
1493
|
+
let tested = null;
|
|
1494
|
+
let cons = null;
|
|
1495
|
+
let consBy = null;
|
|
1496
|
+
let alone = null;
|
|
1497
|
+
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;
|
|
1500
|
+
for (const [bit, said] of [[c.t, c.tCons], [c.f, c.fCons]]) {
|
|
1501
|
+
if (!hasFact(bits, bit)) continue;
|
|
1502
|
+
cons = meet(cons, said);
|
|
1503
|
+
consBy ||= c;
|
|
1504
|
+
if (!alone && stops(said, sinkKind)) alone = c;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
// A check whose outcome puts the index past either end is the use it should have prevented.
|
|
1508
|
+
if (limit != null && outside(cons, limit)) return { level: 0, check: null };
|
|
1509
|
+
if (cons && stops(cons, sinkKind)) return { level: 2, check: alone || consBy };
|
|
1510
|
+
return tested ? { level: 1, check: tested } : { level: 0, check: null };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const outside = (cons, limit) => !!cons && (
|
|
1514
|
+
(cons.lo != null && (cons.lo > limit || (cons.lo === limit && !cons.loInc)))
|
|
1515
|
+
|| (cons.hi != null && (cons.hi < 1 || (cons.hi === 1 && !cons.hiInc))));
|