@portll/cobolwork 0.6.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.
Files changed (140) hide show
  1. package/README.md +49 -18
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +30 -7
  4. package/lib/bms.mjs +21 -7
  5. package/lib/build.mjs +108 -53
  6. package/lib/capabilities.mjs +9 -10
  7. package/lib/cics-commands.mjs +501 -9
  8. package/lib/compliance.mjs +11 -0
  9. package/lib/consequence.mjs +13 -0
  10. package/lib/control-workers.mjs +161 -0
  11. package/lib/control.mjs +52 -3
  12. package/lib/dataflow.mjs +176 -125
  13. package/lib/db2/cursor.mjs +15 -0
  14. package/lib/db2/read.mjs +170 -0
  15. package/lib/db2/rules.mjs +77 -0
  16. package/lib/db2/stmt/alter.mjs +473 -0
  17. package/lib/db2/stmt/grant.mjs +125 -0
  18. package/lib/db2/stmt/index.mjs +141 -0
  19. package/lib/db2/stmt/misc.mjs +325 -0
  20. package/lib/db2/stmt/routine.mjs +564 -0
  21. package/lib/db2/stmt/storage.mjs +146 -0
  22. package/lib/db2/stmt/table.mjs +540 -0
  23. package/lib/db2/stmt/view.mjs +146 -0
  24. package/lib/diff.mjs +17 -5
  25. package/lib/evidence/cli.mjs +14 -3
  26. package/lib/evidence/record.mjs +1 -1
  27. package/lib/evidence/store.mjs +44 -31
  28. package/lib/exec-reading.mjs +51 -0
  29. package/lib/execution.mjs +3 -2
  30. package/lib/explain.mjs +2 -0
  31. package/lib/exploitability.mjs +11 -2
  32. package/lib/hlasm/asm/data.mjs +13 -1
  33. package/lib/hlasm/asm/sections.mjs +3 -1
  34. package/lib/hlasm/instr.mjs +25 -0
  35. package/lib/hlasm/macro/authorization.mjs +24 -4
  36. package/lib/hlasm/macro/datasets.mjs +30 -17
  37. package/lib/hlasm/macro/io.mjs +68 -45
  38. package/lib/hlasm/macro/le.mjs +2 -2
  39. package/lib/hlasm/macro/operator.mjs +32 -21
  40. package/lib/hlasm/macro/program.mjs +126 -84
  41. package/lib/hlasm/macro/recovery.mjs +10 -5
  42. package/lib/hlasm/macro/storage.mjs +64 -14
  43. package/lib/hlasm/macro/structured.mjs +1 -1
  44. package/lib/hlasm/model.mjs +35 -10
  45. package/lib/hlasm/mvs38.mjs +47 -0
  46. package/lib/hlasm/operands.mjs +10 -0
  47. package/lib/hlasm/optable.mjs +61 -0
  48. package/lib/hlasm/read.mjs +22 -8
  49. package/lib/hlasm.mjs +44 -7
  50. package/lib/ims/dli.mjs +37 -0
  51. package/lib/ims/macro/dbd.mjs +299 -0
  52. package/lib/ims/macro/psb.mjs +286 -0
  53. package/lib/ims/model.mjs +149 -0
  54. package/lib/ims/operands.mjs +23 -0
  55. package/lib/ims/read.mjs +37 -0
  56. package/lib/ims/rules.mjs +135 -0
  57. package/lib/ironwork.mjs +17 -13
  58. package/lib/kernel/pds-archive.mjs +256 -0
  59. package/lib/kernel/registry.mjs +14 -8
  60. package/lib/kernel/shared-pass.mjs +34 -9
  61. package/lib/kernel/source-tree.mjs +104 -36
  62. package/lib/kernel/version-key.mjs +17 -0
  63. package/lib/layout.mjs +26 -31
  64. package/lib/parser.mjs +136 -17
  65. package/lib/pli/cursor.mjs +15 -0
  66. package/lib/pli/expr.mjs +101 -0
  67. package/lib/pli/include.mjs +82 -0
  68. package/lib/pli/layout.mjs +125 -0
  69. package/lib/pli/lex.mjs +198 -0
  70. package/lib/pli/program.mjs +280 -0
  71. package/lib/pli/rules/based.mjs +95 -0
  72. package/lib/pli/rules/conditions.mjs +68 -0
  73. package/lib/pli/rules/entry.mjs +130 -0
  74. package/lib/pli/rules/index.mjs +24 -0
  75. package/lib/pli/rules/preprocessor.mjs +55 -0
  76. package/lib/pli/statements.mjs +130 -0
  77. package/lib/pli/stmt/alloc.mjs +45 -0
  78. package/lib/pli/stmt/assignment.mjs +56 -0
  79. package/lib/pli/stmt/call.mjs +104 -0
  80. package/lib/pli/stmt/conditions.mjs +94 -0
  81. package/lib/pli/stmt/control.mjs +219 -0
  82. package/lib/pli/stmt/declare.mjs +149 -0
  83. package/lib/pli/stmt/exec.mjs +55 -0
  84. package/lib/pli/stmt/io.mjs +239 -0
  85. package/lib/pli/stmt/misc.mjs +4 -0
  86. package/lib/pli/stmt/preprocessor.mjs +242 -0
  87. package/lib/pli/stmt/procedure.mjs +258 -0
  88. package/lib/pli/stmt/stream.mjs +283 -0
  89. package/lib/pli/storage.mjs +129 -0
  90. package/lib/precompile-check.mjs +124 -0
  91. package/lib/precompile-cics.mjs +7 -3
  92. package/lib/reach.mjs +11 -2
  93. package/lib/revision.json +1 -1
  94. package/lib/sarif.mjs +41 -3
  95. package/lib/scan.mjs +7 -1
  96. package/lib/sets/abend.mjs +16 -6
  97. package/lib/sets/cics.mjs +15 -35
  98. package/lib/sets/compile.mjs +41 -15
  99. package/lib/sets/crypto.mjs +5 -3
  100. package/lib/sets/ddl.mjs +36 -0
  101. package/lib/sets/flow.mjs +18 -1
  102. package/lib/sets/hidden.mjs +5 -3
  103. package/lib/sets/hlasm.mjs +72 -12
  104. package/lib/sets/ims.mjs +139 -0
  105. package/lib/sets/log.mjs +6 -6
  106. package/lib/sets/opaque.mjs +27 -7
  107. package/lib/sets/pli.mjs +40 -0
  108. package/lib/sets/recon.mjs +5 -3
  109. package/lib/sets/secrets.mjs +5 -3
  110. package/lib/sets/semantics.mjs +3 -0
  111. package/lib/sets/web.mjs +36 -22
  112. package/lib/site.mjs +10 -0
  113. package/lib/sources.mjs +80 -20
  114. package/lib/statement-cursor.mjs +67 -0
  115. package/lib/verify.mjs +3 -2
  116. package/lib/version.mjs +6 -0
  117. package/package.json +3 -2
  118. package/rules/compliance-cobit2019.json +432 -1
  119. package/rules/compliance-dora.json +414 -1
  120. package/rules/compliance-ffiec.json +414 -1
  121. package/rules/compliance-nist80053.json +466 -1
  122. package/rules/hlasm-optables.json +8024 -0
  123. package/schema/cobolwork-baseline.schema.json +36 -0
  124. package/schema/cobolwork-build-provenance.schema.json +187 -0
  125. package/schema/cobolwork-build.schema.json +382 -0
  126. package/schema/cobolwork-capabilities.schema.json +239 -0
  127. package/schema/cobolwork-diff.schema.json +217 -0
  128. package/schema/cobolwork-evidence.schema.json +161 -0
  129. package/schema/cobolwork-execution.schema.json +53 -0
  130. package/schema/cobolwork-explain.schema.json +360 -0
  131. package/schema/cobolwork-finding.schema.json +465 -0
  132. package/schema/cobolwork-flow.schema.json +465 -0
  133. package/schema/cobolwork-gate.schema.json +211 -0
  134. package/schema/cobolwork-inventory.schema.json +206 -0
  135. package/schema/cobolwork-parse.schema.json +105 -0
  136. package/schema/cobolwork-reach.schema.json +74 -0
  137. package/schema/cobolwork-report.schema.json +559 -0
  138. package/schema/cobolwork-witness.schema.json +107 -0
  139. package/schema/cobolwork.baseline.schema.json +101 -0
  140. package/schema/cobolwork.site.schema.json +116 -0
@@ -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 = {};
@@ -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': [],
@@ -134,6 +135,8 @@ const RULE_CLASSES = {
134
135
  'hlasm-executes-built-instruction': [],
135
136
  'hlasm-cross-memory-service': [],
136
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],
137
140
  // Disclosure in transit; severity decides.
138
141
  'web-client-opens-cleartext': [],
139
142
  'web-receive-length-exceeds-area': [MUTATES],
@@ -141,6 +144,16 @@ const RULE_CLASSES = {
141
144
  'icsf-single-length-des-key': [],
142
145
  'icsf-weak-hash': [],
143
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': [],
144
157
  };
145
158
 
146
159
  // Coverage and context assert no defect, so they carry no consequence.
@@ -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.toks.filter((x) => x.t === 'word').map((x) => x.u);
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 may fill what any option is given directly, except what it sends FROM and the
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' && option !== 'FROM' && !reads.includes(option)) add(t);
718
+ if (depth === 1 && t.t === 'word' && fills(option)) add(t);
712
719
  }
713
720
  }
714
721
  return out;
@@ -2976,6 +2983,48 @@ export function sparseFacts(bits) {
2976
2983
  return out;
2977
2984
  }
2978
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
+ }
3027
+
2979
3028
  // What a node's checks say at a point, given the facts that hold there: 2 when the value it holds is
2980
3029
  // safe for the sink, 1 when a check has run on every route to the point, 0 when neither. The check
2981
3030
  // named is the one that decided it: one whose outcome alone makes the value safe, if there is one.