@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
@@ -36,6 +36,7 @@ import { basename, dirname, relative, resolve, sep } from 'node:path';
36
36
  import { buildFileIndex, parseSource } from '../parser.mjs';
37
37
  import { classifyUnder, decodeSource, diskClassifier, heldClassifier, kindOfBytes, looksEbcdic, readSource, relPath } from '../sources.mjs';
38
38
  import { revisionBlobs, treeOf, treePathParts } from './git.mjs';
39
+ import { archiveKind, readArchive } from './pds-archive.mjs';
39
40
 
40
41
  // The shape every adapter answers to. Checked rather than documented, because an adapter missing a
41
42
  // method fails at the first rule set that happens to call it rather than at the boundary.
@@ -70,6 +71,7 @@ export function directoryTree(root, opts = {}) {
70
71
  const kindOf = diskClassifier();
71
72
  classifyUnder(root, kindOf);
72
73
  if (top !== root) classifyUnder(top, kindOf);
74
+ const parseSpec = { copyDirs: idx.copyDirs, fileIndex: idx.index, indexRoot: idx.index.root, systemDirs };
73
75
 
74
76
  return {
75
77
  kind: 'directory',
@@ -81,6 +83,7 @@ export function directoryTree(root, opts = {}) {
81
83
  list: () => [...idx.index.values()].sort(),
82
84
  bytes: (p) => readFileSync(p),
83
85
  text: (p) => readSource(p),
86
+ decode: decodeSource,
84
87
  rel: (p) => relPath(root, p),
85
88
  // The same prefix test buildFileIndex applies while walking. It is not the containment
86
89
  // mechanism - the walk is, and it resolves symlinks before testing - it is the answer to
@@ -88,17 +91,26 @@ export function directoryTree(root, opts = {}) {
88
91
  contains: (p) => { const r = resolve(p); return r === top || r.startsWith(top + sep); },
89
92
  // One parse configuration, in one place. `text` is optional: a caller that has already read
90
93
  // the source passes it rather than reading twice.
91
- parse: (file, text) => parseSource(text ?? readSource(file).text, file, {
92
- format: 'auto',
93
- includeDirs: idx.copyDirs,
94
- fileIndex: idx.index,
95
- mainDir: dirname(file),
96
- copyFormat: 'auto',
97
- systemDirs,
98
- }),
94
+ parse: (file, text) => parseInDirectory(parseSpec, file, text),
95
+ // The same configuration as data, for a worker thread to parse as this tree does.
96
+ parseSpec,
99
97
  };
100
98
  }
101
99
 
100
+ // A directory tree's parse, from its parseSpec. A spec cloned into a worker loses the index's root,
101
+ // which the parser reads to refuse a COPY that leaves the tree, so it is put back.
102
+ export function parseInDirectory(spec, file, text) {
103
+ if (spec.fileIndex.root !== spec.indexRoot) spec.fileIndex.root = spec.indexRoot;
104
+ return parseSource(text ?? readSource(file).text, file, {
105
+ format: 'auto',
106
+ includeDirs: spec.copyDirs,
107
+ fileIndex: spec.fileIndex,
108
+ mainDir: dirname(file),
109
+ copyFormat: 'auto',
110
+ systemDirs: spec.systemDirs,
111
+ });
112
+ }
113
+
102
114
  // A tree whose files are held rather than on disk. The parser finds copybooks in the index built
103
115
  // here and reads them through `readText`, so a COPY resolves to a held file or to a system copy
104
116
  // library outside the tree, never to a file on disk that happens to sit under this tree's root.
@@ -135,6 +147,7 @@ function heldTree({ kind, root, store, rel, systemDirs = [], copyDirs = null, cl
135
147
  list: () => [...store.keys()].sort(),
136
148
  bytes,
137
149
  text,
150
+ decode: decodeSource,
138
151
  rel,
139
152
  // Containment by construction: a path this tree does not hold is a path outside it.
140
153
  contains: (p) => store.has(p),
@@ -206,24 +219,26 @@ export function unframeRecords(buf) {
206
219
  return looksEbcdic(ebcdic) ? ebcdic : lines(0x0A);
207
220
  }
208
221
 
209
- // A member is read from the file the walk found. A link put in its place since, or a file that is
210
- // not a regular one, is refused rather than followed or waited on.
211
- function readMember(file) {
222
+ // A file the walk found, read as it is or its first `limit` bytes. A link put in its place since, or
223
+ // a file that is not a regular one, is refused rather than followed or waited on.
224
+ function readFound(file, limit = Infinity) {
212
225
  const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
213
226
  try {
214
227
  const st = fstatSync(fd);
215
228
  if (!st.isFile()) throw Object.assign(new Error(`not a regular file: ${file}`), { code: 'ENOTFILE' });
216
- const buf = Buffer.alloc(st.size);
229
+ const size = Math.min(st.size, limit);
230
+ const buf = Buffer.alloc(size);
217
231
  let n = 0;
218
- while (n < st.size) {
219
- const r = readSync(fd, buf, n, st.size - n, n);
232
+ while (n < size) {
233
+ const r = readSync(fd, buf, n, size - n, n);
220
234
  if (!r) break;
221
235
  n += r;
222
236
  }
223
- return unframeRecords(buf.subarray(0, n));
237
+ return buf.subarray(0, n);
224
238
  } finally { closeSync(fd); }
225
239
  }
226
240
 
241
+
227
242
  const QUALIFIER = /^[A-Z@#$][A-Z0-9@#$-]{0,7}$/;
228
243
  const MEMBER = /^([A-Z@#$][A-Z0-9@#$]{0,7})(?:\.[^.]*)?$/i;
229
244
  const MAX_LISTED = 8;
@@ -243,58 +258,111 @@ const MAX_LISTED = 8;
243
258
  export function pdsExportTree(dir, opts = {}) {
244
259
  const idx = buildFileIndex(dir);
245
260
  const top = realpathSync(dir);
246
- const root = `${resolve(dir)}@pds`;
261
+ const files = [...idx.index.values()].sort().map((file) => {
262
+ let real;
263
+ try { real = realpathSync(file); } catch { real = file; }
264
+ return { rel: relPath(dir, file), escapes: real !== top && !real.startsWith(top + sep), read: (limit) => readFound(real, limit) };
265
+ });
266
+ return exportOf({ dir, root: `${resolve(dir)}@pds`, files, systemDirs: opts.systemDirs, symlinks: idx.symlinks,
267
+ unreadableDirs: idx.unreadableDirs.map((d) => relative(dir, d)) });
268
+ }
269
+
270
+ // The same export as a git revision holds it, read with plumbing into memory, as gitTree reads one.
271
+ export function pdsExportRevision(repo, ref, opts = {}) {
272
+ const oid = treeOf(repo, ref);
273
+ const { blobs } = revisionBlobs(repo, oid, ref);
274
+ const files = blobs.filter((b) => treePathParts(b.path)).map((b) => ({ rel: b.path, read: (limit = Infinity) => b.bytes.subarray(0, limit) }));
275
+ const at = `${resolve(repo)}@${oid.slice(0, 12)}`;
276
+ return Object.assign(exportOf({ dir: at, root: `${at}@pds`, files, systemDirs: opts.systemDirs }), { ref, oid });
277
+ }
278
+
279
+ // An export's files, each { rel, read(limit), escapes }, held as members: a file whose directory is a
280
+ // data set name and whose name a member name, or an XMIT file or IEBCOPY unload read into its data
281
+ // set. `rel` is the file's path from the export's top, with forward slashes.
282
+ function exportOf({ dir, root, files, systemDirs = [], symlinks = { followed: 0, outside: 0, broken: 0 }, unreadableDirs = [] }) {
247
283
  const origins = new Map();
248
284
  const found = new Map();
249
285
  const notMembers = [];
250
286
  const duplicates = [];
251
287
  const dataSets = new Set();
252
- for (const file of [...idx.index.values()].sort()) {
253
- const where = relative(dir, dirname(file)).split(sep).filter(Boolean);
254
- const dsn = where.join('.').toUpperCase();
255
- const member = MEMBER.exec(basename(file));
288
+ const archived = new Map();
289
+ const archives = { files: 0, members: 0, aliases: 0, notRead: [] };
290
+ for (const { rel, read, escapes } of files) {
291
+ const parts = rel.split('/');
292
+ const file = parts.pop();
293
+ const dsn = parts.join('.').toUpperCase();
294
+ let head = null;
295
+ try { head = archiveKind(read(16)); } catch { head = null; }
296
+ if (head) {
297
+ if (escapes) { notMembers.push(rel); continue; }
298
+ let a;
299
+ try { a = readArchive(read()); } catch (e) { a = { why: reasonOf(e) }; }
300
+ const name = a.dataSet || file.replace(/\.[^.]*$/, '').toUpperCase();
301
+ if (!a.why && !(name.length <= 44 && name.split('.').every((q) => QUALIFIER.test(q)))) a.why = `${name} is not a data set name`;
302
+ if (a.why) { archives.notRead.push(`${rel}: ${a.why}`); continue; }
303
+ archives.files++;
304
+ archives.aliases += a.aliases.length;
305
+ if (a.unmatched) archives.notRead.push(`${rel}: ${a.unmatched} member(s) whose blocks no directory entry names`);
306
+ if (a.missing) archives.notRead.push(`${rel}: no data for ${a.missing.join(', ')}`);
307
+ for (const m of a.members) {
308
+ const at = m.name === null ? resolve(root, name) : resolve(root, name, m.name);
309
+ const origin = m.name === null ? rel : `${rel}(${m.name})`;
310
+ if (found.has(at)) { duplicates.push(`${origin}: ${relPath(root, at)} is ${found.get(at)}`); continue; }
311
+ archived.set(at, m.bytes);
312
+ found.set(at, origin);
313
+ dataSets.add(m.name === null ? at : dirname(at));
314
+ archives.members++;
315
+ }
316
+ continue;
317
+ }
318
+ const member = MEMBER.exec(file);
256
319
  if (!member || dsn.length > 44 || (dsn && !dsn.split('.').every((q) => QUALIFIER.test(q)))) {
257
- notMembers.push(relPath(dir, file));
320
+ notMembers.push(rel);
258
321
  continue;
259
322
  }
260
323
  const at = resolve(root, ...(dsn ? [dsn] : []), member[1].toUpperCase());
261
- if (found.has(at)) { duplicates.push(`${relPath(dir, file)}: ${relPath(root, at)} is ${found.get(at)}`); continue; }
262
- let real;
263
- try { real = realpathSync(file); } catch { real = file; }
264
- if (real !== top && !real.startsWith(top + sep)) { notMembers.push(relPath(dir, file)); continue; }
265
- origins.set(at, real);
266
- found.set(at, relPath(dir, file));
324
+ if (found.has(at)) { duplicates.push(`${rel}: ${relPath(root, at)} is ${found.get(at)}`); continue; }
325
+ if (escapes) { notMembers.push(rel); continue; }
326
+ origins.set(at, read);
327
+ found.set(at, rel);
267
328
  dataSets.add(dirname(at));
268
329
  }
269
330
  const kinds = new Map();
270
331
  const byKind = {};
271
332
  const copyDirs = new Set();
272
- for (const [at, file] of origins) {
333
+ const bytesOf = (at) => archived.get(at) ?? unframeRecords(origins.get(at)());
334
+ for (const at of [...origins.keys(), ...archived.keys()]) {
273
335
  let kind = null;
274
- try { kind = kindOfBytes(readMember(file)); } catch { kind = null; }
336
+ try { kind = kindOfBytes(bytesOf(at)); } catch { kind = null; }
275
337
  kinds.set(at, kind);
276
338
  byKind[kind || 'other'] = (byKind[kind || 'other'] || 0) + 1;
277
339
  if (kind !== 'jcl' && kind !== 'bms') copyDirs.add(dirname(at));
278
340
  }
279
341
  const store = {
280
- keys: () => origins.keys(),
281
- has: (p) => origins.has(p),
282
- get: (p) => (origins.has(p) ? readMember(origins.get(p)) : undefined),
342
+ keys: () => [...origins.keys(), ...archived.keys()].values(),
343
+ has: (p) => origins.has(p) || archived.has(p),
344
+ get: (p) => (origins.has(p) || archived.has(p) ? bytesOf(p) : undefined),
283
345
  };
284
346
  const tree = heldTree({
285
- kind: 'pds-export', root, store, rel: (p) => relPath(root, p), systemDirs: opts.systemDirs || [],
286
- copyDirs: [...copyDirs], classify: () => (p) => kinds.get(p) ?? null, symlinks: idx.symlinks,
287
- unreadableDirs: idx.unreadableDirs.map((d) => resolve(root, relative(dir, d))),
347
+ kind: 'pds-export', root, store, rel: (p) => relPath(root, p), systemDirs,
348
+ copyDirs: [...copyDirs], classify: () => (p) => kinds.get(p) ?? null, symlinks,
349
+ unreadableDirs: unreadableDirs.map((d) => resolve(root, d)),
288
350
  });
289
351
  return Object.assign(tree, {
290
352
  dir,
291
353
  origin: (p) => found.get(p) ?? null,
292
354
  export: {
293
355
  dataSets: dataSets.size,
294
- members: origins.size,
356
+ members: origins.size + archived.size,
295
357
  byKind,
296
358
  ...(notMembers.length ? { notMembers: notMembers.length, notMembersListed: notMembers.slice(0, MAX_LISTED) } : {}),
297
359
  ...(duplicates.length ? { duplicates: duplicates.length, duplicatesListed: duplicates.slice(0, MAX_LISTED) } : {}),
360
+ ...(archives.files || archives.notRead.length ? {
361
+ archives: {
362
+ files: archives.files, members: archives.members, aliases: archives.aliases,
363
+ ...(archives.notRead.length ? { notRead: archives.notRead.length, notReadListed: archives.notRead.slice(0, MAX_LISTED) } : {}),
364
+ },
365
+ } : {}),
298
366
  },
299
367
  });
300
368
  }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The version key and unknown keys of a document cobolwork reads. A document without the key
3
+ // predates versioning and is read as version 0, which holds the same keys as version 1.
4
+ import { printable } from './printable.mjs';
5
+
6
+ // Why the document cannot be read, as a clause to follow its name, or null when it can.
7
+ export function versionProblem(raw, current) {
8
+ const version = raw.version === undefined ? 0 : raw.version;
9
+ if (!Number.isInteger(version) || version < 0) return `has version ${printable(JSON.stringify(version), 40)}, which is not a whole number from 0`;
10
+ if (version > current) return `is version ${version}, and this cobolwork reads up to version ${current}; upgrade cobolwork`;
11
+ return null;
12
+ }
13
+
14
+ // A key opening with an underscore is a note for people, as diag/propose-site.mjs writes them.
15
+ export const unknownKeys = (raw, known) => Object.keys(raw).filter((k) => !known.has(k) && !k.startsWith('_')).sort();
16
+
17
+ export const ignoredKey = (where, key) => `${where}: ${printable(JSON.stringify(key), 80)} is not a key this cobolwork reads, so it was ignored`;
package/lib/layout.mjs CHANGED
@@ -22,6 +22,8 @@ function picInfo(pic, constants) {
22
22
  if (ch === 'V' || ch === 'P') continue;
23
23
  if (ch === '9') { info.digits += n; info.display += n; continue; }
24
24
  if (ch === 'N' || ch === 'G') { info.national = true; info.display += 2 * n; continue; }
25
+ // A U is one UTF-8 character, for which IBM reserves four bytes (Defining UTF-8 data items).
26
+ if (ch === 'U') { info.utf8 = true; info.display += 4 * n; continue; }
25
27
  if (ch === 'X' || ch === 'A') info.alphanumeric = true;
26
28
  info.display += n;
27
29
  }
@@ -45,9 +47,13 @@ function binaryBytes(digits, scheme) {
45
47
  }
46
48
 
47
49
  function elementarySize(item, scheme, constants) {
50
+ if (item.byteLength) return item.byteLength;
48
51
  if (!item.picture && !item.usage) {
49
52
  const constBytes = constants && constants.textBytes;
50
- const fromValue = item.values.reduce((n, v) => n + (v.t === 'lit' ? literalBytes(v) : v.t === 'word' && constBytes && constBytes.has(v.u) ? constBytes.get(v.u) : 0), 0);
53
+ const bytes = (v) => (v.t === 'lit' ? literalBytes(v) : v.t === 'word' && constBytes && constBytes.has(v.u) ? constBytes.get(v.u) : 0);
54
+ // A screen item shows a numeric VALUE as written, one position per character.
55
+ const digits = (v) => v.t === 'num' || (v.t === 'word' && /^\d+$/.test(v.v));
56
+ const fromValue = item.values.reduce((n, v) => n + (item.section === 'SCREEN' && digits(v) ? v.v.length : bytes(v)), 0);
51
57
  if (item.section === 'SCREEN' && !fromValue && item.screenRefItem) return item.screenRefItem.size || 0;
52
58
  if (item.section === 'SCREEN') return Math.max(1, fromValue);
53
59
  if (fromValue) return fromValue;
@@ -147,47 +153,36 @@ export function computeSizes(roots, scheme, constants) {
147
153
  for (const r of roots) place(r, 0);
148
154
  }
149
155
 
150
- // A report group is laid out by column, not by adding up its items: a line is as wide as the column
151
- // its rightmost item ends in, COLUMN PLUS counting on from the end of the item before. A group holds
152
- // its lines one after another, and every 01 group of a report, like the file the report is written
153
- // to, is as large as the largest group.
156
+ // A report group is as large as the larger of its items laid end to end and the column its
157
+ // rightmost elementary item ends in, COLUMN PLUS counting on from the end of the item before; an
158
+ // elementary item keeps its picture's size. Every 01 group of a report, like the file the report is
159
+ // written to, is as large as the largest group. These are GnuCOBOL's sizes: IBM's Report Writer is a
160
+ // precompiler that writes its own records.
154
161
  export function layoutReport(rd) {
155
162
  const structural = (x) => x.children.filter(c => c.level !== 88 && c.level !== 66 && c.level !== 78);
156
163
  const mark = (x) => { x.rd = rd; for (const c of x.children) mark(c); };
157
164
  for (const g of rd.groups) mark(g);
158
- let width = 0;
159
- // Items printed at the same column under PRESENT WHEN each keep their own storage, so a line is
160
- // never smaller than its items laid end to end.
161
- const lineWidth = (line) => {
165
+ const sizeOf = (x) => {
166
+ const kids = structural(x);
167
+ if (!kids.length) return x.size || 0;
162
168
  let last = 0;
163
169
  let right = 0;
164
170
  let total = 0;
165
- const place = (c) => {
166
- const len = c.contributes || c.size || 0;
171
+ for (const c of kids) {
172
+ const len = sizeOf(c) * (c.occurs || 1);
173
+ total += len;
174
+ if (structural(c).length) continue;
167
175
  const start = c.rwColumn ? (c.rwColumn.at != null ? c.rwColumn.at : last + c.rwColumn.plus) : last + 1;
168
176
  last = start + len - 1;
169
177
  right = Math.max(right, last);
170
- total += len;
171
- };
172
- const walk = (x) => { for (const c of structural(x)) { if (structural(c).length) walk(c); else place(c); } };
173
- if (structural(line).length) walk(line);
174
- else place(line);
175
- return Math.max(right, total);
176
- };
177
- // The entries that open a line; a group with no LINE clause anywhere in it is one line.
178
- for (const g of rd.groups) {
179
- const lines = [];
180
- const collect = (x) => { if (x.rwLine) { lines.push(x); return; } for (const c of structural(x)) collect(c); };
181
- collect(g);
182
- if (!lines.length) lines.push(g);
183
- let total = 0;
184
- for (const line of lines) {
185
- const w = lineWidth(line);
186
- if (line !== g) { line.size = w; line.contributes = w * (line.occurs || 1); }
187
- total += w;
188
178
  }
189
- width = Math.max(width, total);
190
- }
179
+ const size = Math.max(right, total);
180
+ x.size = size;
181
+ x.contributes = size * (x.occurs || 1);
182
+ return size;
183
+ };
184
+ let width = 0;
185
+ for (const g of rd.groups) width = Math.max(width, sizeOf(g));
191
186
  for (const g of rd.groups) { g.size = width; g.contributes = width; }
192
187
  rd.width = width;
193
188
  }
package/lib/parser.mjs CHANGED
@@ -210,7 +210,8 @@ function evaluateCondition(text, defines) {
210
210
  return null;
211
211
  }
212
212
 
213
- export function normalize(src, format, defines, std) {
213
+ // A debugging line (D in the indicator) is source under WITH DEBUGGING MODE and a comment otherwise.
214
+ export function normalize(src, format, defines, std, debugging = false) {
214
215
  // Micro Focus reads a free-form line with * or / in column 1 as a comment, as cobc -std=mf does.
215
216
  const columnOneComments = std === 'mf' || std === 'mf-strict';
216
217
  const phys = src.split(/\r?\n/);
@@ -310,7 +311,7 @@ export function normalize(src, format, defines, std) {
310
311
  text = l.slice(7, 7 + width);
311
312
  if (!FIXED_INDICATORS.has(indicator)) { diags.push({ kind: 'invalid-indicator', line }); indicator = ' '; }
312
313
  }
313
- if (indicator === '*' || indicator === '/' || indicator === 'D' || indicator === 'd') continue;
314
+ if (indicator === '*' || indicator === '/' || ((indicator === 'D' || indicator === 'd') && !debugging)) continue;
314
315
  if (fmt === 'free' && (/^\s*\*>/.test(text) || (columnOneComments && /^[*/]/.test(text)))) continue;
315
316
  // AUTHOR, INSTALLATION, DATE-WRITTEN, DATE-COMPILED, SECURITY and REMARKS take a comment-entry:
316
317
  // any text, a COPY or an apostrophe included. It runs to the end of the header's line, and in
@@ -409,8 +410,9 @@ export function tokenize(norm, file) {
409
410
  if (c === ' ' || c === '\t' || c === '\f' || c === '\r') { i++; continue; }
410
411
  if (exec && exec.kind === 'SQL' && c === '-' && s[i + 1] === '-') break;
411
412
  if (picPending) {
413
+ // = is no PICTURE symbol, so == closes the pseudo-text a picture string ends.
412
414
  let j = i;
413
- while (j < n && s[j] !== ' ') j++;
415
+ while (j < n && s[j] !== ' ' && !(s[j] === '=' && s[j + 1] === '=')) j++;
414
416
  let pic = s.slice(i, j);
415
417
  if (pic.toUpperCase() === 'IS') { emit(tok('word', pic, line, file)); i = j; continue; }
416
418
  let trailingPeriod = false;
@@ -853,7 +855,7 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
853
855
  // A copybook is read in the format in force where the COPY statement sits, which a >>SOURCE
854
856
  // directive earlier in the including file may have changed from the file's starting format.
855
857
  const fmt = detectFormat(src) === 'terminal' && ctx.copyFormat === 'auto' ? 'terminal' : (at.fmt || (ctx.copyFormat !== 'auto' ? ctx.copyFormat : (inheritedFormat || ctx.mainFormat)));
856
- const norm = normalize(src, fmt, ctx.defines, ctx.std);
858
+ const norm = normalize(src, fmt, ctx.defines, ctx.std, ctx.debugging);
857
859
  // A tag written against other text - :TAG:-FIELD, FS-(), 'X'-CLE - is replaced in the text, as the
858
860
  // compiler does, so the text around it joins the replacement into one word. A literal is taken as a
859
861
  // tag only where it touches a word; elsewhere it is replaced token for token like any operand.
@@ -896,6 +898,53 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
896
898
  return out;
897
899
  }
898
900
 
901
+ // REPLACE ==:TAG:== BY ==text== replaces the colon-delimited operand wherever the program text holds
902
+ // it, inside a word or a PICTURE as much as standing alone (IBM Enterprise COBOL, REPLACE statement,
903
+ // replacement rules). Like a tag in COPY REPLACING it is replaced in the text, from the statement on
904
+ // until REPLACE OFF or a REPLACE that does not name it again; a REPLACE itself is left as written.
905
+ const TAG = /^:[A-Za-z0-9_-]+:$/;
906
+
907
+ function replaceStatementAt(text, at) {
908
+ const rest = text.slice(at);
909
+ const off = /^REPLACE\s+OFF\s*\./i.exec(rest);
910
+ if (off) return { end: at + off[0].length, off: true, tags: [] };
911
+ const head = /^REPLACE(\s+ALSO)?\s*/i.exec(rest);
912
+ let k = head[0].length;
913
+ const tags = [];
914
+ let pairs = 0;
915
+ for (;;) {
916
+ const pair = /^(?:(?:LEADING|TRAILING)\s+)?==([\s\S]*?)==\s*BY\s*==([\s\S]*?)==\s*/i.exec(rest.slice(k));
917
+ if (!pair) break;
918
+ pairs++;
919
+ if (TAG.test(pair[1].trim())) tags.push([pair[1].trim(), pair[2].trim().replace(/\s+/g, ' ')]);
920
+ k += pair[0].length;
921
+ }
922
+ if (!pairs) return null;
923
+ if (rest[k] === '.') k++;
924
+ return { end: at + k, also: !!head[1], tags };
925
+ }
926
+
927
+ function replaceTags(norm) {
928
+ const text = norm.entries.map((e) => e.text).join('\n');
929
+ if (!/==\s*:[A-Za-z0-9_-]+:\s*==/.test(text)) return;
930
+ let active = [];
931
+ let out = '';
932
+ let from = 0;
933
+ // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- TAG admits no metacharacter
934
+ const swap = (chunk) => active.reduce((t, [tag, to]) => t.replace(new RegExp(tag, 'gi'), to), chunk);
935
+ for (const m of text.matchAll(/\bREPLACE\b/gi)) {
936
+ if (m.index < from) continue;
937
+ const st = replaceStatementAt(text, m.index);
938
+ if (!st) continue;
939
+ out += swap(text.slice(from, m.index)) + text.slice(m.index, st.end);
940
+ active = st.off ? [] : st.also ? [...active, ...st.tags] : st.tags;
941
+ from = st.end;
942
+ }
943
+ out += swap(text.slice(from));
944
+ const lines = out.split('\n');
945
+ if (lines.length === norm.entries.length) norm.entries.forEach((e, i) => { e.text = lines[i]; });
946
+ }
947
+
899
948
  // A REPLACE governs the source up to the next one, which replaces it unless ALSO; OFF ends it; output is not rescanned.
900
949
  function applyReplaceStatements(tokens, ctx) {
901
950
  let active = [];
@@ -976,6 +1025,8 @@ function parseDataEntry(toks, section, file) {
976
1025
  continue;
977
1026
  }
978
1027
  if (u === 'PIC' || u === 'PICTURE') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) item.picture = toks[j].v; j++; continue; }
1028
+ // PIC U BYTE-LENGTH n: a UTF-8 item of n bytes holding as many characters as fit.
1029
+ if (u === 'BYTE-LENGTH') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j] && /^\d+$/.test(toks[j].v)) { item.byteLength = Number(toks[j].v); j++; } continue; }
979
1030
  if (u === 'USAGE') { j++; if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) { item.usage = toks[j].u; j++; } continue; }
980
1031
  if (USAGE_WORDS.has(u)) { item.usage = u; j++; continue; }
981
1032
  // OCCURS DYNAMIC [CAPACITY IN name] [FROM n] [TO m]: sized at its most, none without TO; the
@@ -1048,6 +1099,9 @@ function parseDataEntry(toks, section, file) {
1048
1099
  if (u === 'CONSTANT') { item.constant = true; j++; if (toks[j] && toks[j].u === 'AS') j++; while (toks[j] && (toks[j].t === 'num' || toks[j].t === 'lit' || (toks[j].t === 'word' && /^\d+$/.test(toks[j].v)))) item.values.push(toks[j++]); continue; }
1049
1100
  if (u === 'VALUE' || u === 'VALUES') {
1050
1101
  j++;
1102
+ // A screen item's VALUE is one literal; what follows it is the item's other clauses, and a later
1103
+ // VALUE (two entries run together by a missing period) replaces it.
1104
+ if (section === 'SCREEN') { if (toks[j] && toks[j].u === 'IS') j++; if (toks[j]) { item.values = [toks[j]]; j++; } continue; }
1051
1105
  while (toks[j] && (toks[j].t !== 'word' || !(DATA_CLAUSE_WORDS.has(toks[j].u) || (report && REPORT_CLAUSE_WORDS.has(toks[j].u))) || toks[j].u === 'IS')) { item.values.push(toks[j]); j++; }
1052
1106
  continue;
1053
1107
  }
@@ -1073,6 +1127,25 @@ function debugItem(at) {
1073
1127
  return [root, ...root.children];
1074
1128
  }
1075
1129
 
1130
+ // DEBUG-CONTENTS is X(n), n left to the compiler (IBM, DEBUG-ITEM). GnuCOBOL makes it as long as the
1131
+ // longest item USE FOR DEBUGGING names, a CD's records included, and never shorter than 30.
1132
+ function sizeDebugContents(prog, tokens, from, to, scheme, constants) {
1133
+ const named = [];
1134
+ for (let i = from; i < to - 3; i++) {
1135
+ if (tokens[i].u !== 'USE' || tokens[i + 1].u !== 'FOR' || tokens[i + 2].u !== 'DEBUGGING') continue;
1136
+ for (let k = i + 3; k < to && tokens[k].t !== 'period'; k++) if (tokens[k].t === 'word') named.push(tokens[k].u);
1137
+ }
1138
+ const sizes = named.flatMap((n) => prog.items.filter((it) => it.name === n && it.level < 50 && !it.implicit).map((it) => it.size || 0)
1139
+ .concat(prog.items.filter((it) => it.cd === n).map((it) => it.size || 0)));
1140
+ const longest = Math.max(30, ...sizes);
1141
+ if (longest === 30) return;
1142
+ const root = prog.items.find((it) => it.implicit && it.name === 'DEBUG-ITEM');
1143
+ const contents = root && root.children.find((c) => c.name === 'DEBUG-CONTENTS');
1144
+ if (!contents) return;
1145
+ contents.picture = `X(${longest})`;
1146
+ computeSizes([root], scheme, constants);
1147
+ }
1148
+
1076
1149
  // TYPE gives an item the picture, usage and subordinate items of the TYPEDEF it names. The listing
1077
1150
  // prints the typed item alone; what it holds is implied, reachable by qualification (RE OF WS-Z),
1078
1151
  // so the copies are returned as items marked typeClone.
@@ -1169,6 +1242,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1169
1242
  if (s[m]) f.assign = { t: s[m].t, v: s[m].t === 'word' ? s[m].u : s[m].v };
1170
1243
  }
1171
1244
  for (let m = n + 1; m < s.length; m++) if (s[m].t === 'word') f.envRefs.push(s[m]);
1245
+ f.lineSequential = s.some((t, m) => t.u === 'LINE' && s[m + 1] && s[m + 1].u === 'SEQUENTIAL');
1172
1246
  prog.files.push(f);
1173
1247
  } else {
1174
1248
  for (const t of s) if (t.t === 'word') prog.refs.push({ tok: t, zone: 'env' });
@@ -1182,6 +1256,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1182
1256
  let stack = [];
1183
1257
  let currentFd = null;
1184
1258
  let currentRd = null;
1259
+ let currentCd = null;
1185
1260
  const dataSentences = sentences(tokens, dataStart, dataEnd);
1186
1261
  for (let s of dataSentences) {
1187
1262
  // What an EXEC SQL INCLUDE brings in follows it in the same sentence, up to its own first period.
@@ -1196,6 +1271,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1196
1271
  section = first.u === 'RD' ? 'REPORT' : 'COMMUNICATION';
1197
1272
  currentFd = null;
1198
1273
  currentRd = first.u === 'RD' ? { name: s[1].u, line: first.line, file: first.file, groups: [] } : null;
1274
+ currentCd = first.u === 'CD' ? s[1].u : null;
1199
1275
  if (currentRd) (prog.reports ||= []).push(currentRd);
1200
1276
  else (prog.cds ||= []).push(s[1].u);
1201
1277
  stack = [];
@@ -1204,12 +1280,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1204
1280
  if (first.t === 'word' && (first.u === 'FD' || first.u === 'SD') && s[1]) {
1205
1281
  section = 'FILE';
1206
1282
  currentRd = null;
1207
- currentFd = { kind: first.u, name: s[1].u, line: first.line, file: first.file, records: [], fdTok: s[1], declaredMax: 0, varying: false };
1208
- const recAt = s.findIndex(x => x.t === 'word' && x.u === 'RECORD');
1209
- if (recAt >= 0) for (let m = recAt + 1; m < s.length && !['LABEL', 'BLOCK', 'DATA', 'VALUE', 'RECORDING', 'CODE-SET', 'LINAGE', 'REPORT', 'REPORTS', 'DEPENDING'].includes(s[m].u); m++) {
1210
- if (s[m].u === 'VARYING') currentFd.varying = true;
1211
- if (/^\d+$/.test(s[m].v)) currentFd.declaredMax = Math.max(currentFd.declaredMax, Number(s[m].v));
1212
- }
1283
+ currentFd = { kind: first.u, name: s[1].u, line: first.line, file: first.file, records: [], fdTok: s[1], ...recordClause(s) };
1213
1284
  const repAt = s.findIndex(x => x.t === 'word' && (x.u === 'REPORT' || x.u === 'REPORTS'));
1214
1285
  if (repAt >= 0) {
1215
1286
  currentFd.reports = [];
@@ -1225,6 +1296,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1225
1296
  }
1226
1297
  if ((first.t === 'word' || first.t === 'num') && /^\d{1,2}$/.test(first.v)) {
1227
1298
  const item = parseDataEntry(s, section, first.file);
1299
+ if (section === 'COMMUNICATION' && item.level === 1) item.cd = currentCd;
1228
1300
  if (item.level === 88 || item.level === 66) {
1229
1301
  const parent = item.level === 88 ? stack[stack.length - 1] : null;
1230
1302
  if (parent) { parent.children.push(item); item.parent = parent; }
@@ -1283,6 +1355,7 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1283
1355
  || siblings.find(x => x !== it && x.name === it.redefines) || null;
1284
1356
  }
1285
1357
  computeSizes(roots, scheme, constants);
1358
+ if (prog.debuggingMode && procAt >= 0) sizeDebugContents(prog, tokens, procAt, procEnd, scheme, constants);
1286
1359
  const subtreeCache = new Map();
1287
1360
  const subtree = (record) => {
1288
1361
  if (subtreeCache.has(record)) return subtreeCache.get(record);
@@ -1319,12 +1392,18 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1319
1392
  it.renamesSpan = all.filter(x => x.offset >= from && x.offset + x.contributes <= to && x !== it);
1320
1393
  }
1321
1394
  for (const rd of prog.reports || []) layoutReport(rd);
1322
- // A file is as large as the longer of its declared length and its record description; the
1323
- // compiler warns when a record exceeds the declared maximum but still uses the record. A report
1324
- // file's record is as wide as the widest line of the reports written to it.
1395
+ // A file's record is as long as its RECORD clause allows, or its longest record description
1396
+ // where that is longer or there is no clause; a record longer than the clause still wins, as the
1397
+ // compiler warns and uses it. The FD of a line-sequential file, which GnuCOBOL has and IBM does
1398
+ // not, is sized by its records whatever RECORD CONTAINS says, though RECORD IS VARYING still sets
1399
+ // its maximum. A report file's record is as wide as the widest line of the reports written to it.
1325
1400
  for (const fd of prog.fds || []) {
1326
1401
  const reports = (fd.reports || []).map(n => (prog.reports || []).find(r => r.name === n)).filter(Boolean);
1327
- fd.size = Math.max(fd.declaredMax || 0, 0, ...fd.records.map(r => r.size || 0), ...reports.map(r => r.width || 0));
1402
+ const described = Math.max(0, ...fd.records.map(recordLength), ...reports.map(r => r.width || 0));
1403
+ const select = prog.files.find(f => f.name === fd.name);
1404
+ const ignored = select && select.lineSequential && fd.kind === 'FD' && fd.recordFormat !== 3;
1405
+ const clause = ignored ? 0 : fd.fixedLength || fd.varyingMax || fd.rangeMax || 0;
1406
+ fd.size = Math.max(clause, described);
1328
1407
  }
1329
1408
  }
1330
1409
 
@@ -1334,6 +1413,34 @@ function parseProgram(tokens, from, to, scheme, defines, hostVariables = true) {
1334
1413
  return prog;
1335
1414
  }
1336
1415
 
1416
+ const FD_CLAUSES = new Set(['LABEL', 'BLOCK', 'DATA', 'VALUE', 'RECORDING', 'CODE-SET', 'LINAGE', 'REPORT', 'REPORTS', 'DEPENDING', 'RECORD']);
1417
+
1418
+ // The RECORD clause of an FD in IBM's three formats, CONTAINS n, CONTAINS n TO m and IS VARYING ...
1419
+ // TO m, each giving the longest record the file holds. LABEL RECORD and DATA RECORD are other clauses
1420
+ // that share the word.
1421
+ function recordClause(s) {
1422
+ for (let at = 1; at < s.length; at++) {
1423
+ if (s[at].u !== 'RECORD' || ['LABEL', 'DATA'].includes(s[at - 1].u)) continue;
1424
+ let varying = false;
1425
+ let to = false;
1426
+ const nums = [];
1427
+ let toNum = null;
1428
+ for (let m = at + 1; m < s.length && !FD_CLAUSES.has(s[m].u); m++) {
1429
+ if (s[m].u === 'VARYING') varying = true;
1430
+ else if (s[m].u === 'TO') to = true;
1431
+ else if (/^\d+$/.test(s[m].v)) { nums.push(Number(s[m].v)); if (to && toNum == null) toNum = Number(s[m].v); }
1432
+ }
1433
+ if (varying) return { recordFormat: 3, varyingMax: toNum };
1434
+ if (nums.length === 1 && !to) return { recordFormat: 1, fixedLength: nums[0] };
1435
+ if (nums.length) return { recordFormat: 2, rangeMax: nums.at(-1) };
1436
+ }
1437
+ return { recordFormat: null };
1438
+ }
1439
+
1440
+ // A record's length in its file. OCCURS on a level-01 record is a GnuCOBOL extension IBM refuses;
1441
+ // GnuCOBOL sizes the file by one occurrence.
1442
+ const recordLength = (r) => (r.occurs > 1 ? r.size / r.occurs : r.size || 0);
1443
+
1337
1444
  export function segmentEnd(tokens, i, to) {
1338
1445
  let depth = 0;
1339
1446
  for (let k = i + 1; k < to; k++) {
@@ -1485,10 +1592,15 @@ function parseProcedure(tokens, procAt, to, prog, hostVariables) {
1485
1592
  const constText = target.t === 'word' && prog.constants && prog.constants.text
1486
1593
  ? prog.constants.text.get(target.u) : undefined;
1487
1594
  const resolved = target.t === 'lit' ? target.v : constText;
1595
+ // A literal program name is padded to its field; the compiler drops the trailing spaces. A
1596
+ // literal holding a path calls the program its last component names, as the runtime loads it.
1597
+ const literal = resolved === undefined ? null : String(resolved).trimEnd();
1598
+ const name = literal === null ? target.u : literal.split(/[\\/]/).pop();
1488
1599
  prog.calls.push({
1489
- // A literal program name is padded to its field; the compiler drops the trailing spaces.
1490
1600
  kind: resolved === undefined ? 'I' : 'L',
1491
- name: resolved === undefined ? target.u : String(resolved).trimEnd(),
1601
+ name,
1602
+ ...(literal !== null && name !== literal ? { calledAs: literal } : {}),
1603
+ ...(target.t === 'lit' && target.prefix === 'X' ? { hex: true } : {}),
1492
1604
  ...(constText !== undefined ? { viaConstant: target.u } : {}),
1493
1605
  line: t.line, file: t.file, targetTok: target, using: args, stmtIndex: prog.statements.length - 1,
1494
1606
  });
@@ -1953,8 +2065,15 @@ export function parseSource(src, file, opts = {}) {
1953
2065
  // Where a copybook's text comes from: the disk, or the source tree the program was read from.
1954
2066
  readText: opts.readText || ((p) => readSource(p).text) };
1955
2067
  collectCopybookDefines(src, format, ctx, 0);
1956
- const norm = normalize(src, format, ctx.defines, opts.std);
2068
+ let norm = normalize(src, format, ctx.defines, opts.std);
2069
+ // WITH DEBUGGING MODE in SOURCE-COMPUTER makes the debugging lines of the program and its
2070
+ // copybooks source; it is read from the text with those lines still comments.
2071
+ if (/\bSOURCE-COMPUTER\s*\.[^.]*\bDEBUGGING\s+MODE\b/i.test(norm.entries.map(e => e.text).join(' '))) {
2072
+ ctx.debugging = true;
2073
+ norm = normalize(src, format, ctx.defines, opts.std, true);
2074
+ }
1957
2075
  ctx.mainFormat = norm.finalFormat;
2076
+ replaceTags(norm);
1958
2077
  const { tokens: raw, diags } = tokenize(norm, file);
1959
2078
  ctx.diags.push(...norm.diags.map(d => ({ ...d, file })), ...diags);
1960
2079
  const expanded = stripDirecting(applyReplaceStatements(expand(raw, ctx, [resolve(file)], norm.finalFormat), ctx));
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The PL/I statement cursor: the shared token cursor, failing with PliSyntax.
3
+ import { tokenCursor, split } from '../statement-cursor.mjs';
4
+
5
+ export class PliSyntax extends Error {
6
+ constructor(message, tok) {
7
+ super(message);
8
+ this.name = 'PliSyntax';
9
+ this.line = tok ? tok.line : null;
10
+ this.col = tok ? tok.col : null;
11
+ }
12
+ }
13
+
14
+ export const cursor = (toks) => tokenCursor(toks, PliSyntax);
15
+ export { split };