@portll/cobolwork 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -26
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +56 -14
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +133 -53
- package/lib/capabilities.mjs +13 -11
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +17 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +77 -4
- package/lib/dataflow.mjs +402 -152
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +26 -7
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +44 -15
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +128 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +161 -0
- package/lib/hlasm/asm/symbols.mjs +78 -0
- package/lib/hlasm/exec.mjs +27 -0
- package/lib/hlasm/expr.mjs +167 -0
- package/lib/hlasm/instr.mjs +73 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +146 -0
- package/lib/hlasm/macro/datasets.mjs +107 -0
- package/lib/hlasm/macro/io.mjs +162 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +77 -0
- package/lib/hlasm/macro/program.mjs +184 -0
- package/lib/hlasm/macro/recovery.mjs +85 -0
- package/lib/hlasm/macro/storage.mjs +167 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +97 -0
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +59 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +130 -0
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +20 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +32 -23
- package/lib/kernel/shared-pass.mjs +163 -0
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/options.mjs +26 -4
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/policy.mjs +6 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +8 -4
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +44 -3
- package/lib/scan.mjs +28 -12
- package/lib/sets/abend.mjs +82 -8
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +20 -38
- package/lib/sets/compile.mjs +46 -18
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +32 -8
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +124 -8
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +11 -9
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +112 -0
- package/lib/sets/semantics.mjs +8 -3
- package/lib/sets/web.mjs +39 -23
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +520 -5
- package/rules/compliance-dora.json +509 -5
- package/rules/compliance-ffiec.json +505 -1
- package/rules/compliance-nist80053.json +557 -1
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.policy.schema.json +7 -0
- package/schema/cobolwork.site.schema.json +116 -0
package/lib/compliance.mjs
CHANGED
|
@@ -71,6 +71,17 @@ for (const d of loaded) {
|
|
|
71
71
|
|
|
72
72
|
export const clausesFor = (ruleId) => byRule.get(ruleId) || [];
|
|
73
73
|
|
|
74
|
+
// The clauses each kind of evidence the tools write answers (feed/catalogue.mjs EVIDENCE).
|
|
75
|
+
const byEvidence = new Map();
|
|
76
|
+
for (const d of loaded) {
|
|
77
|
+
for (const r of d.evidence || []) {
|
|
78
|
+
if (!byEvidence.has(r.evidenceId)) byEvidence.set(r.evidenceId, []);
|
|
79
|
+
byEvidence.get(r.evidenceId).push({ framework: r.framework, clause: r.clause, ...(r.title ? { title: r.title } : {}) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const evidenceClausesFor = (evidenceId) => byEvidence.get(evidenceId) || [];
|
|
84
|
+
|
|
74
85
|
// The whole map, in the shape the report's other rule maps use.
|
|
75
86
|
export function complianceMap(ruleIds) {
|
|
76
87
|
const out = {};
|
package/lib/consequence.mjs
CHANGED
|
@@ -59,6 +59,7 @@ const RULE_CLASSES = {
|
|
|
59
59
|
'cics-transfer-to-variable-program': [],
|
|
60
60
|
// A failure carried on from: what follows depends on the program, and severity decides.
|
|
61
61
|
'cics-condition-ignored': [],
|
|
62
|
+
'pli-error-condition-ignored': [],
|
|
62
63
|
// Disclosure of stored secrets and a weaker check; severity decides.
|
|
63
64
|
'program-checks-stored-password': [],
|
|
64
65
|
'password-case-folded-before-compare': [],
|
|
@@ -69,6 +70,7 @@ const RULE_CLASSES = {
|
|
|
69
70
|
'vendor-privileged-command': [ESCALATES],
|
|
70
71
|
'jcl-instream-security-command': [ESCALATES],
|
|
71
72
|
'jcl-instream-credential': [ESCALATES],
|
|
73
|
+
'credential-in-source': [ESCALATES],
|
|
72
74
|
'web-uri-carries-a-credential': [ESCALATES],
|
|
73
75
|
'log-writes-a-credential': [ESCALATES],
|
|
74
76
|
'zowe-config-secret-in-clear': [ESCALATES],
|
|
@@ -122,6 +124,8 @@ const RULE_CLASSES = {
|
|
|
122
124
|
// have landed, where the input chose.
|
|
123
125
|
'input-causes-abend-s0c7': [],
|
|
124
126
|
'input-causes-abend': [],
|
|
127
|
+
'input-causes-hang': [],
|
|
128
|
+
'input-selects-program': [ESCALATES],
|
|
125
129
|
'input-causes-abend-s0c4': [MUTATES],
|
|
126
130
|
'input-causes-abend-subscript-range': [MUTATES],
|
|
127
131
|
// Key zero or supervisor state is the operating system's authority, taken by the code itself.
|
|
@@ -130,6 +134,9 @@ const RULE_CLASSES = {
|
|
|
130
134
|
// the linkage tables grant that, and the source does not show them. Severity decides.
|
|
131
135
|
'hlasm-executes-built-instruction': [],
|
|
132
136
|
'hlasm-cross-memory-service': [],
|
|
137
|
+
'hlasm-runtime-module-name': [ESCALATES],
|
|
138
|
+
// A module FETCH loads by a name held in a variable runs with the program's authority.
|
|
139
|
+
'pli-fetch-title-variable': [ESCALATES],
|
|
133
140
|
// Disclosure in transit; severity decides.
|
|
134
141
|
'web-client-opens-cleartext': [],
|
|
135
142
|
'web-receive-length-exceeds-area': [MUTATES],
|
|
@@ -137,6 +144,16 @@ const RULE_CLASSES = {
|
|
|
137
144
|
'icsf-single-length-des-key': [],
|
|
138
145
|
'icsf-weak-hash': [],
|
|
139
146
|
'icsf-fixed-initialization-vector': [],
|
|
147
|
+
// A definition the generation step or the scheduler refuses stops the program; severity decides.
|
|
148
|
+
'ims-senseg-unknown-segment': [],
|
|
149
|
+
'ims-definition-inconsistent': [],
|
|
150
|
+
// An option granted and not used is authority the program holds, not a change any input makes.
|
|
151
|
+
'ims-procopt-broader-than-used': [],
|
|
152
|
+
// A grant decides who holds a privilege, not that anyone uses it; the PUBLIC grant of a write or an
|
|
153
|
+
// authority is crit, and severity decides the rest.
|
|
154
|
+
'db2-grant-to-public': [],
|
|
155
|
+
'db2-grant-with-grant-option': [],
|
|
156
|
+
'db2-system-authority-granted': [],
|
|
140
157
|
};
|
|
141
158
|
|
|
142
159
|
// Coverage and context assert no defect, so they carry no consequence.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// A program's control analysis, kept so that a byte-identical copy of it elsewhere in the repository
|
|
3
|
+
// is not analysed again.
|
|
4
|
+
//
|
|
5
|
+
// The analysis refers to objects of the program's parse: statements, items, tokens. Kept, each such
|
|
6
|
+
// reference is the object's position in a walk of the parse that visits it in one fixed order, so the
|
|
7
|
+
// copy's parse, walked the same way, supplies the copy's own objects. A string equal to one of the
|
|
8
|
+
// program's file paths is kept as that file's place in the program's list of files, and becomes the
|
|
9
|
+
// copy's path. Anything else the analysis holds is copied as it is. A path inside a longer string, or
|
|
10
|
+
// a value that is not plain data, makes the analysis not kept at all: the copy is analysed afresh.
|
|
11
|
+
|
|
12
|
+
const FAIL = Symbol('not kept');
|
|
13
|
+
|
|
14
|
+
// The objects reachable from root, breadth first: array elements, Map keys and values, Set values and
|
|
15
|
+
// own enumerable properties, in their order. Typed arrays are not entered.
|
|
16
|
+
export function parseOrder(root) {
|
|
17
|
+
const order = [root];
|
|
18
|
+
const seen = new Set(order);
|
|
19
|
+
for (let i = 0; i < order.length; i++) {
|
|
20
|
+
const o = order[i];
|
|
21
|
+
const visit = (v) => { if (v !== null && typeof v === 'object' && !seen.has(v)) { seen.add(v); order.push(v); } };
|
|
22
|
+
if (ArrayBuffer.isView(o)) continue;
|
|
23
|
+
if (Array.isArray(o)) for (const v of o) visit(v);
|
|
24
|
+
else if (o instanceof Map) for (const [k, v] of o) { visit(k); visit(v); }
|
|
25
|
+
else if (o instanceof Set) for (const v of o) visit(v);
|
|
26
|
+
else for (const k of Object.keys(o)) visit(o[k]);
|
|
27
|
+
}
|
|
28
|
+
return order;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const plain = (o) => {
|
|
32
|
+
const proto = Object.getPrototypeOf(o);
|
|
33
|
+
return (proto === Object.prototype || proto === null)
|
|
34
|
+
&& Object.getOwnPropertySymbols(o).length === 0
|
|
35
|
+
&& Object.getOwnPropertyNames(o).length === Object.keys(o).length;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// `files` lists the program's file and its resolved copybooks, in the parse's order.
|
|
39
|
+
export function keep(value, order, files) {
|
|
40
|
+
const at = new Map(order.map((o, i) => [o, i]));
|
|
41
|
+
const fileAt = new Map(files.map((f, i) => [f, i]));
|
|
42
|
+
const table = [];
|
|
43
|
+
const index = new Map();
|
|
44
|
+
const enc = (v) => {
|
|
45
|
+
if (typeof v === 'string') {
|
|
46
|
+
if (fileAt.has(v)) return { f: fileAt.get(v) };
|
|
47
|
+
for (const f of files) if (f && v.includes(f)) throw FAIL;
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
if (v === null || typeof v !== 'object') {
|
|
51
|
+
if (typeof v === 'function' || typeof v === 'symbol') throw FAIL;
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
if (at.has(v)) return { p: at.get(v) };
|
|
55
|
+
if (index.has(v)) return { r: index.get(v) };
|
|
56
|
+
const i = table.length;
|
|
57
|
+
index.set(v, i);
|
|
58
|
+
const entry = {};
|
|
59
|
+
table.push(entry);
|
|
60
|
+
if (ArrayBuffer.isView(v)) { entry.t = v; return { r: i }; }
|
|
61
|
+
if (Array.isArray(v)) {
|
|
62
|
+
if (Object.keys(v).length !== v.length || !plainArray(v)) throw FAIL;
|
|
63
|
+
entry.a = v.map(enc);
|
|
64
|
+
} else if (v instanceof Map) entry.m = [...v].map(([k, x]) => [enc(k), enc(x)]);
|
|
65
|
+
else if (v instanceof Set) entry.s = [...v].map(enc);
|
|
66
|
+
else {
|
|
67
|
+
if (!plain(v)) throw FAIL;
|
|
68
|
+
entry.o = Object.keys(v).map((k) => [k, enc(v[k])]);
|
|
69
|
+
if (Object.getPrototypeOf(v) === null) entry.n = true;
|
|
70
|
+
}
|
|
71
|
+
return { r: i };
|
|
72
|
+
};
|
|
73
|
+
try { return { root: enc(value), table }; } catch (e) { if (e === FAIL) return null; throw e; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const plainArray = (a) => Object.getPrototypeOf(a) === Array.prototype && Object.getOwnPropertySymbols(a).length === 0;
|
|
77
|
+
|
|
78
|
+
export function restore(kept, order, files) {
|
|
79
|
+
const made = kept.table.map((e) => (e.t ? e.t : e.a ? [] : e.m ? new Map() : e.s ? new Set() : e.n ? Object.create(null) : {}));
|
|
80
|
+
const dec = (x) => {
|
|
81
|
+
if (x === null || typeof x !== 'object') return x;
|
|
82
|
+
if ('p' in x) return order[x.p];
|
|
83
|
+
if ('f' in x) return files[x.f];
|
|
84
|
+
return made[x.r];
|
|
85
|
+
};
|
|
86
|
+
kept.table.forEach((e, i) => {
|
|
87
|
+
const o = made[i];
|
|
88
|
+
if (e.a) for (const x of e.a) o.push(dec(x));
|
|
89
|
+
else if (e.m) for (const [k, x] of e.m) o.set(dec(k), dec(x));
|
|
90
|
+
else if (e.s) for (const x of e.s) o.add(dec(x));
|
|
91
|
+
else if (e.o) for (const [k, x] of e.o) o[k] = dec(x);
|
|
92
|
+
});
|
|
93
|
+
return dec(kept.root);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Whether two analyses are the same, objects of the parse compared as themselves.
|
|
97
|
+
export function sameAnalysis(a, b, parse) {
|
|
98
|
+
const pairs = new Map();
|
|
99
|
+
const eq = (x, y) => {
|
|
100
|
+
if (x === y) return true;
|
|
101
|
+
if (x === null || y === null || typeof x !== 'object' || typeof y !== 'object') return Object.is(x, y);
|
|
102
|
+
if (parse.has(x) || parse.has(y)) return false;
|
|
103
|
+
if (pairs.has(x)) return pairs.get(x) === y;
|
|
104
|
+
pairs.set(x, y);
|
|
105
|
+
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
|
|
106
|
+
if (ArrayBuffer.isView(x)) return x.length === y.length && x.every((v, i) => v === y[i]);
|
|
107
|
+
if (x instanceof Map) { if (x.size !== y.size) return false; const ys = [...y]; return [...x].every(([k, v], i) => eq(k, ys[i][0]) && eq(v, ys[i][1])); }
|
|
108
|
+
if (x instanceof Set) { if (x.size !== y.size) return false; const ys = [...y]; return [...x].every((v, i) => eq(v, ys[i])); }
|
|
109
|
+
const kx = Object.keys(x), ky = Object.keys(y);
|
|
110
|
+
return kx.length === ky.length && kx.every((k, i) => k === ky[i] && eq(x[k], y[k]));
|
|
111
|
+
};
|
|
112
|
+
return eq(a, b);
|
|
113
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Control analyses of the programs ahead of the one the flow set is summarising, built in worker
|
|
3
|
+
// threads.
|
|
4
|
+
//
|
|
5
|
+
// A worker parses a file as the directory tree does (tree.parseSpec), runs buildControl on each
|
|
6
|
+
// program in it and posts each analysis kept as lib/control-reuse.mjs keeps a copy's: references into
|
|
7
|
+
// the parse as positions in a fixed walk of it. The main thread restores that onto its own parse of the
|
|
8
|
+
// same file, so the analysis it uses is made of its own objects. The scan stays synchronous: waiting
|
|
9
|
+
// for an answer blocks on a counter the workers raise as they post.
|
|
10
|
+
//
|
|
11
|
+
// A file no worker has started when the main thread reaches it is taken back, and the main thread
|
|
12
|
+
// analyses it itself. Only a started one is waited for.
|
|
13
|
+
import { availableParallelism } from 'node:os';
|
|
14
|
+
import { getHeapStatistics } from 'node:v8';
|
|
15
|
+
import { MessageChannel, Worker, isMainThread, parentPort, receiveMessageOnPort, workerData } from 'node:worker_threads';
|
|
16
|
+
import { buildControl, internFacts, namesOf } from './control.mjs';
|
|
17
|
+
import { keep, parseOrder } from './control-reuse.mjs';
|
|
18
|
+
import { parseInDirectory } from './kernel/source-tree.mjs';
|
|
19
|
+
|
|
20
|
+
export const CONTROL_WORKERS_ENV = 'COBOLWORK_CONTROL_WORKERS';
|
|
21
|
+
// Below this much program text, restoring an analysis on the main thread costs about what building
|
|
22
|
+
// it does: on ACAS, 410 programs, moving every one saved 1 s of 3.3 s.
|
|
23
|
+
export const CONTROL_WORKER_MIN_BYTES = 32 * 1024;
|
|
24
|
+
|
|
25
|
+
// A task is the index of a wanted file. Workers claim tasks in order through a shared counter and
|
|
26
|
+
// start one only once the main thread's limit has passed it, so a task never waits behind a busy
|
|
27
|
+
// worker while another is idle.
|
|
28
|
+
const QUEUED = 0;
|
|
29
|
+
const STARTED = 1;
|
|
30
|
+
const CANCELLED = 2;
|
|
31
|
+
const CLAIM = 0;
|
|
32
|
+
const LIMIT = 1;
|
|
33
|
+
|
|
34
|
+
// The program's file and the copybooks its parse resolved, which the kept analysis names by position.
|
|
35
|
+
export const programFiles = (r, file) => [file, ...r.copies.filter((c) => c.path).map((c) => c.path)];
|
|
36
|
+
|
|
37
|
+
// Each program of a parsed file: { value }, its analysis kept; { none }, no procedure division;
|
|
38
|
+
// { threw }, buildControl threw; { local }, the analysis holds something keep() does not carry.
|
|
39
|
+
export function analyseFile(r, file) {
|
|
40
|
+
const files = programFiles(r, file);
|
|
41
|
+
return r.programs.map((prog) => {
|
|
42
|
+
let ctl;
|
|
43
|
+
try { ctl = buildControl(prog, namesOf(prog).resolve); } catch { return { threw: true }; }
|
|
44
|
+
if (!ctl) return { none: true };
|
|
45
|
+
internFacts(ctl.facts);
|
|
46
|
+
const value = keep(ctl, parseOrder(prog), files);
|
|
47
|
+
return value ? { value } : { local: true };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serve({ spec, port, signal, control, states, tasks }) {
|
|
52
|
+
for (;;) {
|
|
53
|
+
const t = Atomics.add(control, CLAIM, 1);
|
|
54
|
+
if (t >= tasks.length) return;
|
|
55
|
+
for (let limit = Atomics.load(control, LIMIT); t >= limit; limit = Atomics.load(control, LIMIT)) Atomics.wait(control, LIMIT, limit);
|
|
56
|
+
if (Atomics.compareExchange(states, t, QUEUED, STARTED) !== QUEUED) continue;
|
|
57
|
+
let programs = null;
|
|
58
|
+
try { programs = analyseFile(parseInDirectory(spec, tasks[t]), tasks[t]); } catch { /* the main thread's own parse reports it */ }
|
|
59
|
+
try { port.postMessage({ t, programs }); } catch { port.postMessage({ t, programs: null }); }
|
|
60
|
+
Atomics.add(signal, 0, 1);
|
|
61
|
+
Atomics.notify(signal, 0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isMainThread && workerData?.controlWorker) serve(workerData);
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// How many workers a scan starts: COBOLWORK_CONTROL_WORKERS, else one fewer than the machine's
|
|
69
|
+
// threads, at most four. Each holds one program's parse and analysis at a time, up to 1.3 GB on
|
|
70
|
+
// programs of 15 MB.
|
|
71
|
+
export function controlWorkerCount() {
|
|
72
|
+
const n = Number(process.env[CONTROL_WORKERS_ENV]);
|
|
73
|
+
if (process.env[CONTROL_WORKERS_ENV] !== undefined && Number.isInteger(n) && n >= 0) return n;
|
|
74
|
+
return Math.max(0, Math.min(4, availableParallelism() - 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Analyses of `files` built ahead of the main thread, for those `wanted` selects. take(file, index)
|
|
78
|
+
// returns the file's answer, one entry per program, or null where the main thread must analyse it.
|
|
79
|
+
// Files are asked for in order. Workers start at most `ahead` wanted files past the one the main
|
|
80
|
+
// thread is on, since an answer waiting to be taken holds the whole analysis. Twice the workers by
|
|
81
|
+
// default: a large program's analysis takes as long as the main thread's own work on several files,
|
|
82
|
+
// and on cnafbadboy's Batch, scheduled from its measured costs, one more than the workers leaves the
|
|
83
|
+
// main thread waiting a fifth of its loop and twice the workers about 1%.
|
|
84
|
+
//
|
|
85
|
+
// Every failure a worker can catch is answered. One that runs out of heap dies without answering, and
|
|
86
|
+
// nothing the main thread can read while it waits says so. A worker's heap is as large as the main
|
|
87
|
+
// thread's, so a program that exhausts it would exhaust the main thread's too: the wait for a started
|
|
88
|
+
// analysis is long rather than tuned, and past it the main thread analyses the program itself.
|
|
89
|
+
export function controlAhead(spec, files, wanted, { workers, ahead = 2 * workers, waitMs = 30 * 60_000 }) {
|
|
90
|
+
const tasks = [];
|
|
91
|
+
const indexOf = [];
|
|
92
|
+
files.forEach((f, i) => { if (wanted(f)) { tasks.push(f); indexOf.push(i); } });
|
|
93
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
94
|
+
const control = new Int32Array(new SharedArrayBuffer(8));
|
|
95
|
+
const states = new Int32Array(new SharedArrayBuffer(4 * Math.max(1, tasks.length)));
|
|
96
|
+
const maxOldGenerationSizeMb = Math.ceil(getHeapStatistics().heap_size_limit / (1024 * 1024));
|
|
97
|
+
const startWorker = () => {
|
|
98
|
+
const { port1, port2 } = new MessageChannel();
|
|
99
|
+
const worker = new Worker(new URL(import.meta.url), {
|
|
100
|
+
workerData: { controlWorker: true, spec, port: port2, signal, control, states, tasks },
|
|
101
|
+
transferList: [port2],
|
|
102
|
+
resourceLimits: { maxOldGenerationSizeMb },
|
|
103
|
+
});
|
|
104
|
+
worker.on('error', () => {});
|
|
105
|
+
worker.unref();
|
|
106
|
+
port1.unref();
|
|
107
|
+
return { worker, port: port1 };
|
|
108
|
+
};
|
|
109
|
+
const pool = tasks.length ? Array.from({ length: workers }, startWorker) : [];
|
|
110
|
+
// The first task the main thread has neither taken nor passed, and the answers from it on.
|
|
111
|
+
let next = 0;
|
|
112
|
+
const answers = new Map();
|
|
113
|
+
const drain = () => {
|
|
114
|
+
for (const w of pool) {
|
|
115
|
+
for (let m = receiveMessageOnPort(w.port); m; m = receiveMessageOnPort(w.port)) {
|
|
116
|
+
if (m.message.t >= next) answers.set(m.message.t, m.message.programs);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const pass = () => {
|
|
121
|
+
Atomics.compareExchange(states, next, QUEUED, CANCELLED);
|
|
122
|
+
answers.delete(next++);
|
|
123
|
+
};
|
|
124
|
+
const offer = (upTo) => {
|
|
125
|
+
const limit = Math.min(tasks.length, upTo);
|
|
126
|
+
if (limit <= Atomics.load(control, LIMIT)) return;
|
|
127
|
+
Atomics.store(control, LIMIT, limit);
|
|
128
|
+
Atomics.notify(control, LIMIT);
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
take(file, index) {
|
|
132
|
+
drain();
|
|
133
|
+
while (next < tasks.length && indexOf[next] < index) pass();
|
|
134
|
+
if (next >= tasks.length || indexOf[next] !== index) { offer(next + ahead); return null; }
|
|
135
|
+
const t = next;
|
|
136
|
+
const notStarted = Atomics.compareExchange(states, t, QUEUED, CANCELLED) === QUEUED;
|
|
137
|
+
offer(t + 1 + ahead);
|
|
138
|
+
let programs = null;
|
|
139
|
+
if (!notStarted) {
|
|
140
|
+
const deadline = Date.now() + waitMs;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const seen = Atomics.load(signal, 0);
|
|
143
|
+
drain();
|
|
144
|
+
if (answers.has(t) || Date.now() > deadline) break;
|
|
145
|
+
Atomics.wait(signal, 0, seen, 200);
|
|
146
|
+
}
|
|
147
|
+
programs = answers.get(t) ?? null;
|
|
148
|
+
}
|
|
149
|
+
answers.delete(t);
|
|
150
|
+
next = t + 1;
|
|
151
|
+
return programs;
|
|
152
|
+
},
|
|
153
|
+
close() {
|
|
154
|
+
for (let t = next; t < tasks.length; t++) Atomics.compareExchange(states, t, QUEUED, CANCELLED);
|
|
155
|
+
Atomics.store(control, LIMIT, tasks.length);
|
|
156
|
+
Atomics.notify(control, LIMIT);
|
|
157
|
+
for (const w of pool) { w.port.close(); w.worker.terminate(); }
|
|
158
|
+
answers.clear();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/lib/control.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// paragraph, for the same reason.
|
|
22
22
|
import { segmentEnd } from './parser.mjs';
|
|
23
23
|
import { parser, attempt, candidates, leaves, storedRange } from './arith.mjs';
|
|
24
|
+
import { execReading } from './exec-reading.mjs';
|
|
24
25
|
|
|
25
26
|
const WORD = (t, u) => t && t.t === 'word' && t.u === u;
|
|
26
27
|
const PHRASED = new Set(['READ', 'WRITE', 'REWRITE', 'DELETE', 'START', 'RETURN', 'CALL', 'COMPUTE', 'ADD', 'SUBTRACT',
|
|
@@ -267,7 +268,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
267
268
|
const twice = new Set();
|
|
268
269
|
for (const l of prog.labels) { if (seenLabel.has(l.name)) twice.add(l.name); seenLabel.add(l.name); }
|
|
269
270
|
const sure = (name) => labelNames.has(name) && !twice.has(name);
|
|
270
|
-
const wordsOf = (e) => e
|
|
271
|
+
const wordsOf = (e) => execReading(e).words;
|
|
271
272
|
// Once IGNORE CONDITION has run, a RETURN or XCTL that fails comes back to the next statement, and
|
|
272
273
|
// whether it has run by then is the order of execution, not of the source. A HANDLE CONDITION sends
|
|
273
274
|
// the failure to its label, but which one is in force is as uncertain, so reach assumes it may not.
|
|
@@ -698,17 +699,23 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
698
699
|
}
|
|
699
700
|
}
|
|
700
701
|
} else if (n.e) {
|
|
701
|
-
// A command
|
|
702
|
+
// A command fills the options the precompile table says CICS returns a value in. One the table
|
|
703
|
+
// does not hold may fill what any option is given directly, except what it sends FROM and the
|
|
702
704
|
// data-values it reads. A name inside that argument's own parentheses - a subscript, a
|
|
703
705
|
// reference modification - is read.
|
|
704
706
|
const reads = (n.e.kind === 'CICS' && CICS_READS[n.e.toks[0]?.u]) || [];
|
|
707
|
+
const direction = n.e.kind === 'CICS' ? execReading(n.e).direction : () => null;
|
|
708
|
+
const fills = (option) => {
|
|
709
|
+
const d = direction(option);
|
|
710
|
+
return d ? d === 'receives' || d === 'both' : option !== 'FROM' && !reads.includes(option);
|
|
711
|
+
};
|
|
705
712
|
let depth = 0;
|
|
706
713
|
let option = null;
|
|
707
714
|
for (let i = 0; i < n.e.toks.length; i++) {
|
|
708
715
|
const t = n.e.toks[i];
|
|
709
716
|
if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
|
|
710
717
|
if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
|
|
711
|
-
if (depth === 1 && t.t === 'word' &&
|
|
718
|
+
if (depth === 1 && t.t === 'word' && fills(option)) add(t);
|
|
712
719
|
}
|
|
713
720
|
}
|
|
714
721
|
return out;
|
|
@@ -2950,7 +2957,73 @@ function readersOf(tree) {
|
|
|
2950
2957
|
return out;
|
|
2951
2958
|
}
|
|
2952
2959
|
|
|
2953
|
-
|
|
2960
|
+
// A fact set is a Uint32Array with one bit per fact, or, where a program keeps it after its analysis,
|
|
2961
|
+
// a Uint16Array holding the sorted numbers of the facts that hold (sparseFacts).
|
|
2962
|
+
export const hasFact = (bits, x) => x != null && bits != null
|
|
2963
|
+
&& (bits instanceof Uint16Array ? holdsSorted(bits, x) : (bits[x >>> 5] & (1 << (x & 31))) !== 0);
|
|
2964
|
+
function holdsSorted(list, x) {
|
|
2965
|
+
let lo = 0;
|
|
2966
|
+
let hi = list.length - 1;
|
|
2967
|
+
while (lo <= hi) {
|
|
2968
|
+
const mid = (lo + hi) >>> 1;
|
|
2969
|
+
if (list[mid] === x) return true;
|
|
2970
|
+
if (list[mid] < x) lo = mid + 1; else hi = mid - 1;
|
|
2971
|
+
}
|
|
2972
|
+
return false;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// The facts a set holds as their sorted numbers, where every number fits 16 bits; the set itself otherwise.
|
|
2976
|
+
export function sparseFacts(bits) {
|
|
2977
|
+
if (bits.length * 32 > 65536) return bits;
|
|
2978
|
+
let n = 0;
|
|
2979
|
+
for (let i = 0; i < bits.length; i++) for (let w = bits[i]; w; w &= w - 1) n++;
|
|
2980
|
+
const out = new Uint16Array(n);
|
|
2981
|
+
let k = 0;
|
|
2982
|
+
for (let i = 0; i < bits.length; i++) for (let w = bits[i]; w; w &= w - 1) out[k++] = i * 32 + (31 - Math.clz32(w & -w));
|
|
2983
|
+
return out;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// The facts a program keeps per statement: one copy of each distinct set, held as the numbers of the
|
|
2987
|
+
// facts in it, all in one buffer per program. Neighbouring statements mostly hold the same checks, and
|
|
2988
|
+
// few of a program's checks hold at any one statement: 26 large programs kept 1.5 GB of bitsets, 0.4%
|
|
2989
|
+
// of their bits set.
|
|
2990
|
+
export function internFacts(facts) {
|
|
2991
|
+
const byHash = new Map();
|
|
2992
|
+
const kept = [];
|
|
2993
|
+
const keptAt = new Map();
|
|
2994
|
+
for (const [point, bits] of facts) {
|
|
2995
|
+
let h = 2166136261;
|
|
2996
|
+
for (let i = 0; i < bits.length; i++) h = Math.imul(h ^ bits[i], 16777619);
|
|
2997
|
+
let same = byHash.get(h);
|
|
2998
|
+
if (!same) byHash.set(h, same = []);
|
|
2999
|
+
let k = same.find((x) => x.bits.length === bits.length && x.bits.every((v, i) => v === bits[i]));
|
|
3000
|
+
if (!k) { same.push(k = { bits, held: sparseFacts(bits) }); kept.push(k); }
|
|
3001
|
+
keptAt.set(point, k);
|
|
3002
|
+
}
|
|
3003
|
+
let size = 0;
|
|
3004
|
+
for (const k of kept) if (k.held instanceof Uint16Array) size += k.held.length;
|
|
3005
|
+
const pool = new Uint16Array(size);
|
|
3006
|
+
let at = 0;
|
|
3007
|
+
for (const k of kept) {
|
|
3008
|
+
if (!(k.held instanceof Uint16Array)) continue;
|
|
3009
|
+
pool.set(k.held, at);
|
|
3010
|
+
k.held = pool.subarray(at, at += k.held.length);
|
|
3011
|
+
}
|
|
3012
|
+
for (const [point, k] of keptAt) facts.set(point, k.held);
|
|
3013
|
+
return facts;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// The name resolution the flow analysis hands buildControl: the parser's reference resolution, then the
|
|
3017
|
+
// first item of the name, since names inside EXEC blocks never pass through the parser's, then an
|
|
3018
|
+
// INDEXED BY name, which is not a data item.
|
|
3019
|
+
export function namesOf(prog) {
|
|
3020
|
+
const byName = new Map();
|
|
3021
|
+
for (const it of prog.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
3022
|
+
const itemOfToken = (tok) => (tok ? prog.resolved.get(tok) || (tok.t === 'word' ? byName.get(tok.u) || null : null) : null);
|
|
3023
|
+
const indexNames = new Set(prog.items.flatMap((it) => it.indexNames || []));
|
|
3024
|
+
const resolve = (tok) => itemOfToken(tok) || (tok && tok.t === 'word' && indexNames.has(tok.u) ? { index: tok.u } : null);
|
|
3025
|
+
return { byName, itemOfToken, indexNames, resolve };
|
|
3026
|
+
}
|
|
2954
3027
|
|
|
2955
3028
|
// What a node's checks say at a point, given the facts that hold there: 2 when the value it holds is
|
|
2956
3029
|
// safe for the sink, 1 when a check has run on every route to the point, 0 when neither. The check
|