@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.
Files changed (163) hide show
  1. package/README.md +89 -26
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +56 -14
  4. package/lib/baseline.mjs +9 -0
  5. package/lib/bms.mjs +21 -7
  6. package/lib/build.mjs +133 -53
  7. package/lib/capabilities.mjs +13 -11
  8. package/lib/cics-commands.mjs +501 -9
  9. package/lib/compliance.mjs +11 -0
  10. package/lib/consequence.mjs +17 -0
  11. package/lib/control-reuse.mjs +113 -0
  12. package/lib/control-workers.mjs +161 -0
  13. package/lib/control.mjs +77 -4
  14. package/lib/dataflow.mjs +402 -152
  15. package/lib/db2/cursor.mjs +15 -0
  16. package/lib/db2/read.mjs +170 -0
  17. package/lib/db2/rules.mjs +77 -0
  18. package/lib/db2/stmt/alter.mjs +473 -0
  19. package/lib/db2/stmt/grant.mjs +125 -0
  20. package/lib/db2/stmt/index.mjs +141 -0
  21. package/lib/db2/stmt/misc.mjs +325 -0
  22. package/lib/db2/stmt/routine.mjs +564 -0
  23. package/lib/db2/stmt/storage.mjs +146 -0
  24. package/lib/db2/stmt/table.mjs +540 -0
  25. package/lib/db2/stmt/view.mjs +146 -0
  26. package/lib/diff.mjs +17 -5
  27. package/lib/equivalence.mjs +44 -5
  28. package/lib/evidence/cli.mjs +26 -7
  29. package/lib/evidence/record.mjs +16 -6
  30. package/lib/evidence/seal.mjs +17 -0
  31. package/lib/evidence/store.mjs +44 -15
  32. package/lib/evidence/timestamp.mjs +83 -0
  33. package/lib/evidence/verify.mjs +120 -42
  34. package/lib/exec-reading.mjs +51 -0
  35. package/lib/execution.mjs +3 -2
  36. package/lib/explain.mjs +2 -0
  37. package/lib/exploitability.mjs +11 -2
  38. package/lib/hlasm/asm/data.mjs +128 -0
  39. package/lib/hlasm/asm/listing.mjs +43 -0
  40. package/lib/hlasm/asm/output.mjs +45 -0
  41. package/lib/hlasm/asm/sections.mjs +161 -0
  42. package/lib/hlasm/asm/symbols.mjs +78 -0
  43. package/lib/hlasm/exec.mjs +27 -0
  44. package/lib/hlasm/expr.mjs +167 -0
  45. package/lib/hlasm/instr.mjs +73 -0
  46. package/lib/hlasm/locate.mjs +333 -0
  47. package/lib/hlasm/macro/authorization.mjs +146 -0
  48. package/lib/hlasm/macro/datasets.mjs +107 -0
  49. package/lib/hlasm/macro/io.mjs +162 -0
  50. package/lib/hlasm/macro/le.mjs +53 -0
  51. package/lib/hlasm/macro/linkage.mjs +143 -0
  52. package/lib/hlasm/macro/operator.mjs +77 -0
  53. package/lib/hlasm/macro/program.mjs +184 -0
  54. package/lib/hlasm/macro/recovery.mjs +85 -0
  55. package/lib/hlasm/macro/storage.mjs +167 -0
  56. package/lib/hlasm/macro/structured.mjs +131 -0
  57. package/lib/hlasm/model.mjs +97 -0
  58. package/lib/hlasm/mvs38.mjs +47 -0
  59. package/lib/hlasm/operands.mjs +59 -0
  60. package/lib/hlasm/optable.mjs +61 -0
  61. package/lib/hlasm/read.mjs +130 -0
  62. package/lib/hlasm.mjs +44 -7
  63. package/lib/ims/dli.mjs +37 -0
  64. package/lib/ims/macro/dbd.mjs +299 -0
  65. package/lib/ims/macro/psb.mjs +286 -0
  66. package/lib/ims/model.mjs +149 -0
  67. package/lib/ims/operands.mjs +23 -0
  68. package/lib/ims/read.mjs +37 -0
  69. package/lib/ims/rules.mjs +135 -0
  70. package/lib/inventory.mjs +10 -7
  71. package/lib/ironwork-ids.mjs +24 -0
  72. package/lib/ironwork.mjs +20 -13
  73. package/lib/kernel/pds-archive.mjs +256 -0
  74. package/lib/kernel/registry.mjs +32 -23
  75. package/lib/kernel/shared-pass.mjs +163 -0
  76. package/lib/kernel/source-tree.mjs +104 -36
  77. package/lib/kernel/version-key.mjs +17 -0
  78. package/lib/layout.mjs +26 -31
  79. package/lib/options.mjs +26 -4
  80. package/lib/parser.mjs +136 -17
  81. package/lib/pli/cursor.mjs +15 -0
  82. package/lib/pli/expr.mjs +101 -0
  83. package/lib/pli/include.mjs +82 -0
  84. package/lib/pli/layout.mjs +125 -0
  85. package/lib/pli/lex.mjs +198 -0
  86. package/lib/pli/program.mjs +280 -0
  87. package/lib/pli/rules/based.mjs +95 -0
  88. package/lib/pli/rules/conditions.mjs +68 -0
  89. package/lib/pli/rules/entry.mjs +130 -0
  90. package/lib/pli/rules/index.mjs +24 -0
  91. package/lib/pli/rules/preprocessor.mjs +55 -0
  92. package/lib/pli/statements.mjs +130 -0
  93. package/lib/pli/stmt/alloc.mjs +45 -0
  94. package/lib/pli/stmt/assignment.mjs +56 -0
  95. package/lib/pli/stmt/call.mjs +104 -0
  96. package/lib/pli/stmt/conditions.mjs +94 -0
  97. package/lib/pli/stmt/control.mjs +219 -0
  98. package/lib/pli/stmt/declare.mjs +149 -0
  99. package/lib/pli/stmt/exec.mjs +55 -0
  100. package/lib/pli/stmt/io.mjs +239 -0
  101. package/lib/pli/stmt/misc.mjs +4 -0
  102. package/lib/pli/stmt/preprocessor.mjs +242 -0
  103. package/lib/pli/stmt/procedure.mjs +258 -0
  104. package/lib/pli/stmt/stream.mjs +283 -0
  105. package/lib/pli/storage.mjs +129 -0
  106. package/lib/policy.mjs +6 -0
  107. package/lib/precompile-check.mjs +124 -0
  108. package/lib/precompile-cics.mjs +8 -4
  109. package/lib/reach.mjs +11 -2
  110. package/lib/revision.json +1 -1
  111. package/lib/sarif.mjs +44 -3
  112. package/lib/scan.mjs +28 -12
  113. package/lib/sets/abend.mjs +82 -8
  114. package/lib/sets/build.mjs +43 -37
  115. package/lib/sets/cics.mjs +20 -38
  116. package/lib/sets/compile.mjs +46 -18
  117. package/lib/sets/copybook.mjs +5 -3
  118. package/lib/sets/crypto.mjs +5 -3
  119. package/lib/sets/ddl.mjs +36 -0
  120. package/lib/sets/flow.mjs +32 -8
  121. package/lib/sets/hidden.mjs +5 -3
  122. package/lib/sets/hlasm.mjs +124 -8
  123. package/lib/sets/ims.mjs +139 -0
  124. package/lib/sets/log.mjs +11 -9
  125. package/lib/sets/opaque.mjs +27 -7
  126. package/lib/sets/pli.mjs +40 -0
  127. package/lib/sets/priv.mjs +5 -3
  128. package/lib/sets/recon.mjs +5 -3
  129. package/lib/sets/secrets.mjs +112 -0
  130. package/lib/sets/semantics.mjs +8 -3
  131. package/lib/sets/web.mjs +39 -23
  132. package/lib/site.mjs +10 -0
  133. package/lib/sources.mjs +80 -20
  134. package/lib/statement-cursor.mjs +67 -0
  135. package/lib/verify.mjs +3 -2
  136. package/lib/version.mjs +6 -0
  137. package/package.json +3 -2
  138. package/rules/compliance-cobit2019.json +520 -5
  139. package/rules/compliance-dora.json +509 -5
  140. package/rules/compliance-ffiec.json +505 -1
  141. package/rules/compliance-nist80053.json +557 -1
  142. package/rules/gitleaks-mainframe.toml +46 -15
  143. package/rules/hlasm-instructions.json +2396 -0
  144. package/rules/hlasm-optables.json +8024 -0
  145. package/schema/cobolwork-baseline.schema.json +36 -0
  146. package/schema/cobolwork-build-provenance.schema.json +187 -0
  147. package/schema/cobolwork-build.schema.json +382 -0
  148. package/schema/cobolwork-capabilities.schema.json +239 -0
  149. package/schema/cobolwork-diff.schema.json +217 -0
  150. package/schema/cobolwork-evidence.schema.json +161 -0
  151. package/schema/cobolwork-execution.schema.json +53 -0
  152. package/schema/cobolwork-explain.schema.json +360 -0
  153. package/schema/cobolwork-finding.schema.json +465 -0
  154. package/schema/cobolwork-flow.schema.json +465 -0
  155. package/schema/cobolwork-gate.schema.json +211 -0
  156. package/schema/cobolwork-inventory.schema.json +206 -0
  157. package/schema/cobolwork-parse.schema.json +105 -0
  158. package/schema/cobolwork-reach.schema.json +74 -0
  159. package/schema/cobolwork-report.schema.json +559 -0
  160. package/schema/cobolwork-witness.schema.json +107 -0
  161. package/schema/cobolwork.baseline.schema.json +101 -0
  162. package/schema/cobolwork.policy.schema.json +7 -0
  163. package/schema/cobolwork.site.schema.json +116 -0
@@ -0,0 +1,130 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // HLASM source read statement by statement: the cards fold as the assembler folds them (lib/bms.mjs),
3
+ // and each statement goes to the parser for what its operation is. A machine instruction is matched
4
+ // against IBM's operand syntax, an assembler instruction and a system macro each have their own
5
+ // parser, and any other macro call is split into its operands. Conditional assembly and macro
6
+ // definitions are counted and not evaluated: nothing here expands a macro.
7
+ import { foldStatements } from '../bms.mjs';
8
+ import { HlasmSyntax, NAME, macroOperands } from './operands.mjs';
9
+ import { INSTRUCTIONS, parseInstruction, formatFamily } from './instr.mjs';
10
+ import { inOptable } from './optable.mjs';
11
+ import { parsers as data } from './asm/data.mjs';
12
+ import { parsers as sections } from './asm/sections.mjs';
13
+ import { parsers as symbols } from './asm/symbols.mjs';
14
+ import { parsers as listing } from './asm/listing.mjs';
15
+ import { parsers as output } from './asm/output.mjs';
16
+ import { parsers as program } from './macro/program.mjs';
17
+ import { parsers as linkage } from './macro/linkage.mjs';
18
+ import { parsers as storage } from './macro/storage.mjs';
19
+ import { parsers as authorization } from './macro/authorization.mjs';
20
+ import { parsers as operator } from './macro/operator.mjs';
21
+ import { parsers as datasets } from './macro/datasets.mjs';
22
+ import { parsers as io } from './macro/io.mjs';
23
+ import { parsers as recovery } from './macro/recovery.mjs';
24
+ import { parsers as le } from './macro/le.mjs';
25
+ import { parsers as structured } from './macro/structured.mjs';
26
+ import { parsers as exec } from './exec.mjs';
27
+ import { parsers as model } from './model.mjs';
28
+
29
+ // The assembler instructions of HLASM V1R6 (Language Reference SC26-4940-09, chapters 5 and 9).
30
+ export const ASSEMBLER = new Set(['ACONTROL', 'ADATA', 'AINSERT', 'ALIAS', 'AMODE', 'CATTR', 'CCW', 'CCW0', 'CCW1',
31
+ 'CEJECT', 'CNOP', 'COM', 'COPY', 'CSECT', 'CXD', 'DC', 'DROP', 'DS', 'DSECT', 'DXD', 'EJECT', 'END', 'ENTRY', 'EQU',
32
+ 'EXITCTL', 'EXTRN', 'ICTL', 'ISEQ', 'LOCTR', 'LTORG', 'MNOTE', 'OPSYN', 'ORG', 'POP', 'PRINT', 'PUNCH', 'PUSH',
33
+ 'REPRO', 'RMODE', 'RSECT', 'SPACE', 'START', 'TITLE', 'USING', 'WXTRN', 'XATTR']);
34
+ export const CONDITIONAL = new Set(['ACTR', 'AEJECT', 'AGO', 'AGOB', 'AIF', 'AIFB', 'ANOP', 'AREAD', 'ASPACE',
35
+ 'GBLA', 'GBLB', 'GBLC', 'LCLA', 'LCLB', 'LCLC', 'MEXIT', 'MHELP', 'SETA', 'SETAF', 'SETB', 'SETC', 'SETCF']);
36
+ // The z/OS macros the rules read, each parsed against the keywords IBM lists for it.
37
+ export const SYSTEM_MACROS = new Set(['LINK', 'XCTL', 'LOAD', 'ATTACH', 'CALL', 'SAVE', 'RETURN', 'MODESET',
38
+ 'STORAGE', 'GETMAIN', 'FREEMAIN', 'RACROUTE', 'TESTAUTH', 'WTO', 'WTOR', 'OPEN', 'CLOSE', 'DCB', 'GET', 'PUT',
39
+ 'READ', 'WRITE', 'ESTAE', 'ESPIE', 'ABEND', 'SNAP']);
40
+ // Language Environment's prolog, epilog and mapping macros, and the HLASM Toolkit's structured
41
+ // programming macros, each with its own parser.
42
+ export const LE_MACROS = new Set(['CEEENTRY', 'CEETERM', 'CEEPPA', 'CEECAA', 'CEEDSA']);
43
+ export const TOOLKIT_MACROS = new Set(['IF', 'ELSE', 'ELSEIF', 'ENDIF', 'DO', 'ENDDO', 'DOEXIT', 'ASMLEAVE', 'ITERATE',
44
+ 'SELECT', 'WHEN', 'NEXTWHEN', 'OTHRWISE', 'ENDSEL', 'STRTSRCH', 'EXITIF', 'ORELSE', 'ENDLOOP', 'ENDSRCH',
45
+ 'CASENTRY', 'CASE', 'ENDCASE', 'ASMMREL']);
46
+ export const NAMED_MACROS = new Set([...SYSTEM_MACROS, ...LE_MACROS, ...TOOLKIT_MACROS]);
47
+
48
+ const NO_OPERANDS = new Set(['CSECT', 'DSECT', 'RSECT', 'COM', 'LOCTR', 'LTORG', 'EJECT', 'REPRO', 'CXD']);
49
+
50
+ export const PARSERS = Object.freeze({ ...data, ...sections, ...symbols, ...listing, ...output });
51
+ export const MACRO_PARSERS = Object.freeze({ ...program, ...linkage, ...storage, ...authorization, ...operator, ...datasets, ...io, ...recovery, ...le, ...structured });
52
+
53
+ // Statements other than comments, each marked inMacro when it is part of a macro definition, the
54
+ // prototype (the statement after MACRO) marked as such, and sourceMacro when its operation names a
55
+ // macro the file has already defined: a source macro with the name of an instruction or a library
56
+ // macro overrides it (https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=definitions-macro-instruction-prototype).
57
+ // EXEC CICS and EXEC SQL are written for their translators, with blanks between the words: their
58
+ // field is the cards' text after EXEC, columns 16 to 71 of each continuation, not the operands up
59
+ // to the first blank as an assembler statement's are.
60
+ function execField(phys, st) {
61
+ return st.lines.map((n, i) => (i === 0 ? phys[n - 1].slice(0, 71).replace(/^\S*\s+EXEC\b/i, '') : phys[n - 1].slice(15, 71))).join(' ').replace(/\s+/g, ' ').trim();
62
+ }
63
+
64
+ // libraryMacros: the macros the repository's own library defines. One with the name of a macro this
65
+ // reader knows is the repository's, found ahead of the system's on the macro search; an instruction
66
+ // is never looked for in a library.
67
+ export function readHlasmStatements(text, { libraryMacros = null } = {}) {
68
+ const { statements, diags } = foldStatements(text, { batch: true });
69
+ const phys = String(text).replace(/\r\n?/g, '\n').split('\n');
70
+ const out = [];
71
+ let depth = 0;
72
+ let prototypeNext = false;
73
+ const defined = new Set();
74
+ for (const s of statements) {
75
+ if (s.kind === 'comment' || s.kind === 'not-assembler') continue;
76
+ const st = { ...s, inMacro: depth > 0, prototype: false };
77
+ if (s.kind === 'statement' && s.operation === 'EXEC') st.field = execField(phys, s);
78
+ if (s.kind === 'statement') {
79
+ if (s.operation === 'MACRO') { depth++; prototypeNext = true; st.inMacro = true; }
80
+ else if (prototypeNext) { st.prototype = true; prototypeNext = false; if (s.operation) defined.add(s.operation.toUpperCase()); }
81
+ else if (s.operation && defined.has(s.operation.toUpperCase())) st.sourceMacro = true;
82
+ else if (s.operation && libraryMacros?.has(s.operation.toUpperCase()) && NAMED_MACROS.has(s.operation.toUpperCase())) st.libraryMacro = true;
83
+ if (s.operation === 'MEND') depth = Math.max(0, depth - 1);
84
+ // Each program of a batch is its own assembly: its source macros are its own.
85
+ if (s.operation === 'END' && depth === 0) defined.clear();
86
+ }
87
+ out.push(st);
88
+ }
89
+ return { statements: out, diags };
90
+ }
91
+
92
+ function run(kind, parser, st, ...args) {
93
+ if (!parser) return { kind, status: 'unbuilt' };
94
+ try {
95
+ return { kind, status: 'parsed', node: parser(st, ...args) };
96
+ } catch (e) {
97
+ if (e instanceof HlasmSyntax) return { kind, status: 'unparsed', reason: e.message };
98
+ return { kind, status: 'unparsed', reason: `parser error: ${e && e.message}`, crash: true };
99
+ }
100
+ }
101
+
102
+ // { kind, status, node?, reason?, crash? }: parsed, unbuilt (no parser for the kind yet), unparsed
103
+ // (the parser refused it) or unknown (not an operation HLASM has).
104
+ export function parseHlasmStatement(st, opts = {}) {
105
+ if (st.kind !== 'statement') return { kind: 'UNREADABLE', status: 'unknown', reason: 'no operation field' };
106
+ if (st.continues === 'comment') return { kind: 'CONTINUED', status: 'unparsed', reason: 'the comment above runs into column 72, so the assembler reads this line as part of it' };
107
+ if (st.continues === 'statement') return { kind: 'CONTINUED', status: 'unparsed', reason: 'the statement above runs into column 72 and this line does not leave columns 1 to 15 blank' };
108
+ const op = st.operation;
109
+ if (op === 'MACRO' || op === 'MEND') return { kind: 'MACRO DEFINITION', status: 'parsed', node: { kind: op } };
110
+ if (CONDITIONAL.has(op)) return { kind: 'CONDITIONAL', status: 'unbuilt' };
111
+ if (st.prototype) return run('PROTOTYPE', model.PROTOTYPE, st);
112
+ if (st.inMacro) return run('MODEL', model.MODEL, st);
113
+ if (/&[A-Z$#@_]/i.test(`${st.name || ''} ${op} ${st.field || ''}`.replace(/&&/g, ''))) return run('SUBSTITUTED', model.SUBSTITUTED, st);
114
+ if (st.sourceMacro || st.libraryMacro) return run('MACRO CALL', (s) => ({ kind: 'MACRO CALL', macro: op, ...(st.sourceMacro ? { source: true } : { library: true }), ...macroOperands(s.field || '') }), st);
115
+ // A lone comma is the convention for no operands with remarks after it (CSECT , remark); an
116
+ // instruction that takes no operands reads whatever follows it as remarks.
117
+ if (ASSEMBLER.has(op)) return run(op, PARSERS[op], st.field === ',' || NO_OPERANDS.has(op) ? { ...st, field: '' } : st);
118
+ const row = INSTRUCTIONS.get(op);
119
+ if (row && inOptable(op, opts.optable)) return run(formatFamily(row.format), parseInstruction, st, row);
120
+ if (op === 'EXEC') {
121
+ const kind = `EXEC ${(/^\S+/.exec(st.field || '') || ['?'])[0].toUpperCase()}`;
122
+ return run(kind, exec[kind], st);
123
+ }
124
+ if (!NAME.test(op)) return { kind: 'UNKNOWN', status: 'unknown', reason: `${op} is not an operation name` };
125
+ if (NAMED_MACROS.has(op)) {
126
+ const parser = MACRO_PARSERS[op];
127
+ return run(op, parser && ((s) => parser(s, macroOperands(s.field === ',' ? '' : s.field || ''), opts)), st);
128
+ }
129
+ return run('MACRO CALL', (s) => ({ kind: 'MACRO CALL', macro: op, ...macroOperands(s.field || '') }), st);
130
+ }
package/lib/hlasm.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { foldStatements } from './bms.mjs';
9
9
  import { parseOperands, splitOperands } from './cards.mjs';
10
+ import { keyZeroExtkeys, systemKeyExtkeys } from './hlasm/mvs38.mjs';
10
11
 
11
12
  const TABLE = JSON.parse(readFileSync(new URL('../rules/hlasm-operations.json', import.meta.url), 'utf8'));
12
13
  export const OPERATIONS = new Map(TABLE.operations.map((o) => [o.name, o]));
@@ -31,31 +32,64 @@ const IMS = new Set(['DBD', 'DBDGEN', 'SEGM', 'PSBGEN', 'PCB', 'SENSEG']);
31
32
 
32
33
  // MODESET changes the PSW key or the state; these operand values make it key zero or supervisor
33
34
  // state, and the opposite values (KEY=NZERO, MODE=PROB) return from it. EXTKEY is the inline form,
34
- // which sets only the PSW key.
35
+ // which sets only the PSW key; MVS 3.8's names for key zero count when its forms are read.
35
36
  const STATE_CHANGE = { KEY: ['ZERO'], MODE: ['SUP'], EXTKEY: ['ZERO'] };
36
37
 
37
38
  const nameList = (field) => splitOperands(field || '').map((s) => s.trim().toUpperCase()).filter((s) => /^[A-Z$#@_][A-Z0-9$#@_]*$/.test(s));
38
39
 
39
- function stateChange(field) {
40
+ function stateChange(field, opts) {
40
41
  let ops;
41
42
  try { ops = parseOperands(field || ''); } catch { return null; }
42
- for (const [key, values] of Object.entries(STATE_CHANGE)) {
43
+ for (const [key, zero] of Object.entries(STATE_CHANGE)) {
44
+ const values = key === 'EXTKEY' ? [...zero, ...keyZeroExtkeys(opts)] : zero;
43
45
  const v = ops.keywords.get(key);
44
46
  if (v && values.includes(String(v).trim().toUpperCase())) return `${key}=${String(v).trim().toUpperCase()}`;
45
47
  }
46
48
  return null;
47
49
  }
48
50
 
51
+ // EXTKEY= naming a system key other than zero: z/OS's KEY2, KEY3, KEY4 and KEY7, and MVS 3.8's
52
+ // names for keys 1 to 7 when its forms are read. TCB, RBT1 and RBT234 take a key decided at run time.
53
+ const SYSTEM_EXTKEYS = [['KEY2', 2], ['KEY3', 3], ['KEY4', 4], ['KEY7', 7]];
54
+
55
+ function systemKeyChange(field, opts) {
56
+ let ops;
57
+ try { ops = parseOperands(field || ''); } catch { return null; }
58
+ const v = String(ops.keywords.get('EXTKEY') || '').trim().toUpperCase();
59
+ const key = new Map([...SYSTEM_EXTKEYS, ...systemKeyExtkeys(opts)]).get(v);
60
+ return key === undefined ? null : { written: `EXTKEY=${v}`, key };
61
+ }
62
+
63
+ // A disassembler's listing: each line's object code, its characters between asterisks and its offset
64
+ // to the right of the source, as in `BR R15 07FF *..* 00004`.
65
+ const DISASSEMBLED = /\s[0-9A-F]{2,16}(?: [0-9A-F]{2,16}){0,2}\s+\*.{1,16}\*\s+[0-9A-F]{5,8}\s*$/;
66
+
67
+ // English function words, which no statement's name, operation or first operand holds.
68
+ const FUNCTION_WORDS = new Set(['THE', 'OF', 'AND', 'TO', 'IS', 'IN', 'FOR', 'THIS', 'THAT', 'WITH', 'ARE', 'BE', 'IT', 'AN', 'WILL', 'CAN',
69
+ 'YOU', 'WHICH', 'FROM', 'ON', 'AS', 'BY', 'NOT', 'IF', 'WHEN', 'HAS', 'HAVE', 'WAS', 'ALL', 'ANY', 'ONE', 'MAY', 'YOUR', 'THESE', 'THEY',
70
+ 'THERE', 'SHOULD', 'MUST', 'ALSO', 'ONLY']);
71
+
72
+ // A manual, article or README saved as assembler source: its statements, macro definitions included,
73
+ // read as English, and the reader refuses many of those in open code. On the CBT, MVS 3.8j and GitHub
74
+ // corpora both thresholds together held for 25 documents and no program.
75
+ export function isDocument(statements, open, refused) {
76
+ if (open < 5) return false;
77
+ const english = statements.filter((s) => [s.name, s.operation, (s.field || '').split(/[\s,]/)[0]].some((w) => w && FUNCTION_WORDS.has(w.toUpperCase()))).length;
78
+ return english >= 0.15 * statements.length && refused >= 0.2 * open;
79
+ }
80
+
49
81
  // What the file is, and in it: each operation the table names with where it is written; the
50
82
  // sections and entry points it defines, which a CALL or a job step can name; and the names it
51
83
  // leaves to the binder.
52
- export function readHlasm(text) {
53
- const { statements, diags } = foldStatements(text);
84
+ export function readHlasm(text, opts = {}) {
85
+ const { statements, diags } = foldStatements(text, { batch: true });
54
86
  const ops = statements.filter((s) => s.kind === 'statement');
55
87
  const has = (set) => ops.some((s) => set.has(s.operation));
56
88
  const weak = new Set(ops.filter((s) => WEAK.has(s.operation)).map((s) => s.operation)).size;
57
89
  const assembler = !GNU.test(text) && (ops.some(strong) || weak >= 2);
58
- const kind = has(BMS) ? 'bms' : has(IMS) ? 'ims' : assembler ? 'hlasm' : 'unrecognised';
90
+ const lines = String(text).replace(/\r\n?/g, '\n').split('\n');
91
+ const listing = ops.length >= 5 && ops.filter((s) => DISASSEMBLED.test(lines[s.line - 1].slice(0, 80))).length * 2 >= ops.length;
92
+ const kind = has(BMS) ? 'bms' : has(IMS) ? 'ims' : !assembler ? 'unrecognised' : listing ? 'listing' : 'hlasm';
59
93
  const out = { kind, statements: ops.length, operations: [], defines: [], external: [], diags };
60
94
  if (kind !== 'hlasm') return out;
61
95
 
@@ -72,7 +106,10 @@ export function readHlasm(text) {
72
106
  const op = OPERATIONS.get(s.operation);
73
107
  if (!op || op.class === 'section' || op.class === 'linkage') continue;
74
108
  const use = { name: op.name, class: op.class, line: s.line, inMacro, field: s.field };
75
- if (op.name === 'MODESET') use.stateChange = stateChange(s.field);
109
+ if (op.name === 'MODESET') {
110
+ use.stateChange = stateChange(s.field, opts);
111
+ if (!use.stateChange) use.systemKey = systemKeyChange(s.field, opts);
112
+ }
76
113
  out.operations.push(use);
77
114
  }
78
115
  return out;
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The DL/I call interface as a program issues it: CALL 'CBLTDLI' or 'AIBTDLI' USING [count,] function,
3
+ // PCB or AIB, I/O area [, SSA ...], and EXEC DLI. Each database function needs a processing option on
4
+ // the PCB it runs against, the PROCOPT of its PSBGEN PCB or SENSEG: a get needs G, ISRT I, REPL R
5
+ // and DLET D. A get call fills the I/O area with the segment it retrieves.
6
+
7
+ export const DLI_ROUTINE = /^(CBLTDLI|AIBTDLI)$/i;
8
+
9
+ export const DLI_FUNCTIONS = Object.freeze({
10
+ GU: 'G', GN: 'G', GNP: 'G', GHU: 'G', GHN: 'G', GHNP: 'G',
11
+ ISRT: 'I', REPL: 'R', DLET: 'D',
12
+ });
13
+
14
+ export const isGet = (fn) => DLI_FUNCTIONS[fn] === 'G';
15
+
16
+ // Calls that are not database calls: scheduling, termination, checkpoint, restart, rollback, sync,
17
+ // logging, statistics and the system service calls. They need no processing option.
18
+ export const DLI_SERVICES = new Set(['PCB', 'TERM', 'CHKP', 'XRST', 'ROLB', 'ROLL', 'ROLS', 'SETS', 'SETU', 'SYNC', 'INIT',
19
+ 'INQY', 'GSCD', 'LOG', 'STAT', 'APSB', 'DPSB', 'GMSG', 'ICMD', 'RCMD', 'SCHD', 'AUTH', 'CHNG', 'PURG', 'CMD', 'GCMD', 'ICAL']);
20
+
21
+ // The PSB a DFSRRC00 step's PARM names: region type, program, then PSB, which defaults to the program.
22
+ export function psbOfParm(parm) {
23
+ const [, program, psb] = String(parm || '').replace(/^[('\s]+|[)'\s]+$/g, '').split(',').map((x) => x.trim().toUpperCase());
24
+ return program ? { program, psb: psb || program } : null;
25
+ }
26
+
27
+ // The function code of a CALL to a DL/I routine and the argument that holds it: the first argument,
28
+ // or the second when the first is a parameter count. `valueOf(arg)` gives the literal or the VALUE
29
+ // of the item an argument names.
30
+ export function dliFunction(using, valueOf) {
31
+ for (let index = 0; index < Math.min(2, using.length); index++) {
32
+ const v = valueOf(using[index]);
33
+ const fn = v == null ? null : String(v).trim().toUpperCase();
34
+ if (fn && Object.hasOwn(DLI_FUNCTIONS, fn)) return { fn, index };
35
+ }
36
+ return null;
37
+ }
@@ -0,0 +1,299 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Parsers for the IMS DBD macros: DBD, DATASET, AREA, SEGM, FIELD, LCHILD, XDFLD, DBDGEN.
3
+ import { ImsSyntax, sublist, integer, onlyKeywords } from '../operands.mjs';
4
+
5
+ const ACCESS_TYPES = new Set(['HDAM', 'HIDAM', 'HISAM', 'HSAM', 'SHISAM', 'SHSAM', 'PHDAM', 'PHIDAM', 'PSINDEX', 'INDEX', 'LOGICAL', 'DEDB', 'MSDB', 'GSAM']);
6
+ const ACCESS_METHODS = new Set(['VSAM', 'OSAM', 'BSAM']);
7
+ const FIELD_TYPES = new Set(['C', 'X', 'P', 'Z', 'H', 'F', 'G']);
8
+
9
+ function parseDBD(st, ops) {
10
+ const name = ops.keywords.get('NAME');
11
+ if (!name) throw new ImsSyntax('DBD requires NAME');
12
+ const accessRaw = ops.keywords.get('ACCESS');
13
+ let access = null;
14
+ if (accessRaw) {
15
+ const items = sublist(accessRaw);
16
+ if (!items || items.length < 1 || items.length > 3) throw new ImsSyntax('DBD ACCESS must be a type or (type, method[,prot])');
17
+ const type = items[0].toUpperCase();
18
+ if (!ACCESS_TYPES.has(type)) throw new ImsSyntax(`DBD ACCESS type ${type} is not recognised`);
19
+ let method = null;
20
+ if (items.length >= 2) {
21
+ method = items[1].toUpperCase();
22
+ if (!ACCESS_METHODS.has(method)) throw new ImsSyntax(`DBD ACCESS method ${method} is not recognised`);
23
+ }
24
+ access = { type, method };
25
+ }
26
+
27
+ let passwd = null;
28
+ const passwdRaw = ops.keywords.get('PASSWD');
29
+ if (passwdRaw) {
30
+ const p = passwdRaw.toUpperCase();
31
+ if (p !== 'YES' && p !== 'NO') throw new ImsSyntax('DBD PASSWD must be YES or NO');
32
+ passwd = p;
33
+ }
34
+
35
+ let rmname = null;
36
+ const rmnameRaw = ops.keywords.get('RMNAME');
37
+ if (rmnameRaw) {
38
+ const items = sublist(rmnameRaw);
39
+ if (!items || items.length < 1 || items.length > 4) throw new ImsSyntax('DBD RMNAME must be (module[,anchors[,rbn[,bytes]]])');
40
+ rmname = items;
41
+ }
42
+
43
+ const keywords = {};
44
+ for (const [k, v] of ops.keywords) keywords[k] = v;
45
+
46
+ return {
47
+ kind: 'DBD',
48
+ name,
49
+ access,
50
+ passwd,
51
+ rmname,
52
+ keywords
53
+ };
54
+ }
55
+
56
+ function parseDATASET(st, ops) {
57
+ const keywords = {};
58
+ for (const [k, v] of ops.keywords) keywords[k] = v;
59
+
60
+ const dd1 = ops.keywords.get('DD1');
61
+ const dd2 = ops.keywords.get('DD2');
62
+ const ovflw = ops.keywords.get('OVFLW');
63
+ const device = ops.keywords.get('DEVICE');
64
+ const sizeRaw = ops.keywords.get('SIZE');
65
+ let size = null;
66
+ if (sizeRaw) {
67
+ const items = sublist(sizeRaw);
68
+ if (items) size = items;
69
+ else size = [sizeRaw];
70
+ }
71
+
72
+ return {
73
+ kind: 'DATASET',
74
+ label: st.name,
75
+ dd1,
76
+ dd2,
77
+ ovflw,
78
+ device,
79
+ size,
80
+ keywords
81
+ };
82
+ }
83
+
84
+ function parseAREA(st, ops) {
85
+ const keywords = {};
86
+ for (const [k, v] of ops.keywords) keywords[k] = v;
87
+
88
+ return {
89
+ kind: 'AREA',
90
+ keywords
91
+ };
92
+ }
93
+
94
+ function parseSEGM(st, ops) {
95
+ const name = ops.keywords.get('NAME');
96
+ if (!name) throw new ImsSyntax('SEGM requires NAME');
97
+
98
+ const parentRaw = ops.keywords.get('PARENT');
99
+ let parent = null;
100
+ if (parentRaw) {
101
+ const p = parentRaw.trim();
102
+ if (p === '0') {
103
+ parent = null;
104
+ } else {
105
+ const items = sublist(p);
106
+ if (items && items.length === 1) {
107
+ parent = items[0];
108
+ } else {
109
+ parent = p;
110
+ }
111
+ }
112
+ }
113
+
114
+ const bytesRaw = ops.keywords.get('BYTES');
115
+ let bytes = null;
116
+ if (bytesRaw) {
117
+ const items = sublist(bytesRaw);
118
+ if (items) {
119
+ if (items.length === 1) {
120
+ const n = integer(items[0]);
121
+ if (n === null) throw new ImsSyntax('SEGM BYTES must be an integer or (max,min)');
122
+ bytes = { max: n, min: n };
123
+ } else if (items.length === 2) {
124
+ const max = integer(items[0]);
125
+ const min = integer(items[1]);
126
+ if (max === null || min === null) throw new ImsSyntax('SEGM BYTES (max,min) must be integers');
127
+ bytes = { max, min };
128
+ } else {
129
+ throw new ImsSyntax('SEGM BYTES must be an integer or (max,min)');
130
+ }
131
+ } else {
132
+ const n = integer(bytesRaw);
133
+ if (n === null) throw new ImsSyntax('SEGM BYTES must be an integer or (max,min)');
134
+ bytes = { max: n, min: n };
135
+ }
136
+ }
137
+
138
+ const rulesRaw = ops.keywords.get('RULES');
139
+ let rules = null;
140
+ if (rulesRaw) {
141
+ rules = sublist(rulesRaw);
142
+ }
143
+
144
+ const pointerRaw = ops.keywords.get('POINTER');
145
+ let pointer = null;
146
+ if (pointerRaw) {
147
+ const items = sublist(pointerRaw);
148
+ if (items) pointer = items;
149
+ else pointer = pointerRaw;
150
+ }
151
+
152
+ const keywords = {};
153
+ for (const [k, v] of ops.keywords) keywords[k] = v;
154
+
155
+ return {
156
+ kind: 'SEGM',
157
+ name,
158
+ parent,
159
+ bytes,
160
+ rules,
161
+ pointer,
162
+ keywords
163
+ };
164
+ }
165
+
166
+ function parseFIELD(st, ops) {
167
+ const nameRaw = ops.keywords.get('NAME');
168
+ if (!nameRaw) throw new ImsSyntax('FIELD requires NAME');
169
+
170
+ const items = sublist(nameRaw);
171
+ let name, seq = false, unique = null;
172
+ if (items) {
173
+ name = items[0];
174
+ if (items.length >= 2) {
175
+ const seqFlag = items[1].toUpperCase();
176
+ if (seqFlag === 'SEQ') seq = true;
177
+ else throw new ImsSyntax('FIELD NAME sublist second element must be SEQ');
178
+ }
179
+ if (items.length >= 3) {
180
+ const uFlag = items[2].toUpperCase();
181
+ if (uFlag === 'U') unique = true;
182
+ else if (uFlag === 'M') unique = false;
183
+ else throw new ImsSyntax('FIELD NAME sublist third element must be U or M');
184
+ }
185
+ } else {
186
+ name = nameRaw;
187
+ }
188
+
189
+ const bytesRaw = ops.keywords.get('BYTES');
190
+ let bytes = null;
191
+ if (bytesRaw) {
192
+ const n = integer(bytesRaw);
193
+ if (n === null) throw new ImsSyntax('FIELD BYTES must be an integer');
194
+ bytes = n;
195
+ }
196
+
197
+ const startRaw = ops.keywords.get('START');
198
+ let start = null;
199
+ if (startRaw) {
200
+ const n = integer(startRaw);
201
+ if (n === null) throw new ImsSyntax('FIELD START must be an integer');
202
+ start = n;
203
+ }
204
+
205
+ const typeRaw = ops.keywords.get('TYPE');
206
+ let type = 'C';
207
+ if (typeRaw) {
208
+ const t = typeRaw.toUpperCase();
209
+ if (!FIELD_TYPES.has(t)) throw new ImsSyntax(`FIELD TYPE ${t} is not recognised`);
210
+ type = t;
211
+ }
212
+
213
+ const keywords = {};
214
+ for (const [k, v] of ops.keywords) keywords[k] = v;
215
+
216
+ return {
217
+ kind: 'FIELD',
218
+ name,
219
+ seq,
220
+ unique,
221
+ bytes,
222
+ start,
223
+ type,
224
+ keywords
225
+ };
226
+ }
227
+
228
+ function parseLCHILD(st, ops) {
229
+ const nameRaw = ops.keywords.get('NAME');
230
+ if (!nameRaw) throw new ImsSyntax('LCHILD requires NAME');
231
+
232
+ const items = sublist(nameRaw);
233
+ let name, dbd = null;
234
+ if (items) {
235
+ name = items[0];
236
+ if (items.length >= 2) dbd = items[1];
237
+ } else {
238
+ name = nameRaw;
239
+ }
240
+
241
+ const keywords = {};
242
+ for (const [k, v] of ops.keywords) keywords[k] = v;
243
+
244
+ return {
245
+ kind: 'LCHILD',
246
+ name,
247
+ dbd,
248
+ keywords
249
+ };
250
+ }
251
+
252
+ function parseXDFLD(st, ops) {
253
+ const keywords = {};
254
+ for (const [k, v] of ops.keywords) keywords[k] = v;
255
+
256
+ return {
257
+ kind: 'XDFLD',
258
+ keywords
259
+ };
260
+ }
261
+
262
+ function parseDBDGEN(st, ops) {
263
+ const keywords = {};
264
+ for (const [k, v] of ops.keywords) keywords[k] = v;
265
+
266
+ return {
267
+ kind: 'DBDGEN',
268
+ keywords
269
+ };
270
+ }
271
+
272
+ const KEYWORDS = {
273
+ DBD: ['NAME', 'ACCESS', 'RMNAME', 'PASSWD', 'VERSION', 'DATXEXIT', 'EXIT', 'PSNAME', 'GSROOT', 'ENCODING', 'FPINDEX'],
274
+ DATASET: ['DD1', 'DD2', 'OVFLW', 'DEVICE', 'BLOCK', 'SIZE', 'SCAN', 'FRSPC', 'RECORD', 'RECFM', 'MODEL', 'SEARCHA', 'REL'],
275
+ AREA: ['DD1', 'MODEL', 'SIZE', 'UOW', 'ROOT', 'REMOTE'],
276
+ SEGM: ['NAME', 'PARENT', 'BYTES', 'FREQ', 'POINTER', 'PTR', 'RULES', 'COMPRTN', 'EXIT', 'SOURCE', 'SSPTR', 'TYPE', 'DSGROUP'],
277
+ FIELD: ['NAME', 'BYTES', 'START', 'TYPE'],
278
+ LCHILD: ['NAME', 'POINTER', 'PTR', 'PAIR', 'RULES', 'INDEX', 'RKSIZE'],
279
+ XDFLD: ['NAME', 'SEGMENT', 'SRCH', 'SUBSEQ', 'DDATA', 'NULLVAL', 'CONST', 'EXTRTN'],
280
+ DBDGEN: [],
281
+ };
282
+ // Operands that end in a comma were meant to go on, but the next card was not read as their
283
+ // continuation (no column-72 mark), so the operands that follow never reached the parser.
284
+ const checked = (macro, parse) => (st, ops) => {
285
+ if (/,\s*$/.test(st.field)) throw new ImsSyntax(`${macro} operands end in a comma: the operands meant to follow were not read as a continuation`);
286
+ onlyKeywords(ops, macro, KEYWORDS[macro]);
287
+ return parse(st, ops);
288
+ };
289
+
290
+ export const parsers = Object.freeze({
291
+ DBD: checked('DBD', parseDBD),
292
+ DATASET: checked('DATASET', parseDATASET),
293
+ AREA: checked('AREA', parseAREA),
294
+ SEGM: checked('SEGM', parseSEGM),
295
+ FIELD: checked('FIELD', parseFIELD),
296
+ LCHILD: checked('LCHILD', parseLCHILD),
297
+ XDFLD: checked('XDFLD', parseXDFLD),
298
+ DBDGEN: checked('DBDGEN', parseDBDGEN)
299
+ });