@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
@@ -1,142 +1,184 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Parsers for z/OS program-management macros: LINK, XCTL, LOAD, ATTACH.
3
- import { HlasmSyntax, NAME } from '../operands.mjs';
3
+ import { HlasmSyntax, NAME, remoteForm, splitOperands } from '../operands.mjs';
4
+ import { parseAddress } from '../expr.mjs';
5
+ import { MVS38_HIARCHY, MVS38_KEYWORDS, mvs38Form } from '../mvs38.mjs';
4
6
 
5
- const ADDR = /^(?:\(\d{1,2}\)|[A-Z$#@_][A-Z0-9$#@_]*|[\w$#@_]+(?:\s*[-+*/]\s*[\w$#@_]+)*)$/i;
6
- const NUM_OR_ADDR = /^(?:\d+|\(\d{1,2}\)|[A-Z$#@_][A-Z0-9$#@_]*|[\w$#@_]+(?:\s*[-+*/]\s*[\w$#@_]+)*)$/i;
7
+ // An RX-type or A-type address, a register in parentheses, a literal or an absolute expression, as
8
+ // the macros' operands are written: anything the address reader takes.
9
+ const isAddress = (text) => {
10
+ const t = String(text).trim();
11
+ if (t === '') return false;
12
+ try { parseAddress(t); return true; } catch { return false; }
13
+ };
7
14
  const YES_NO = /^(?:YES|NO)$/i;
8
15
 
16
+ // STAI= and ESTAI= of ATTACH: an exit address, or (exit addr,parm addr).
17
+ const isExit = (text) => {
18
+ const t = String(text).trim();
19
+ if (isAddress(t)) return true;
20
+ if (!t.startsWith('(') || !t.endsWith(')')) return false;
21
+ const parts = splitOperands(t.slice(1, -1));
22
+ return parts.length <= 2 && parts.every(isAddress);
23
+ };
24
+
25
+ // The list form (SF=L) and the execute form (MF=(E,addr), SF=(E,addr)) of LINK and XCTL (z/OS 3.1
26
+ // MVS Programming: Assembler Services Reference, Volume 2). The list form may leave the entry point
27
+ // to the execute form, and an execute form with SF=(E,addr) may take it from the list.
28
+ function forms(kw, macro) {
29
+ const sf = kw.has('SF') ? remoteForm(kw.get('SF'), 'SF') : null;
30
+ const mf = kw.has('MF') ? remoteForm(kw.get('MF'), 'MF') : null;
31
+ if (mf && mf.form !== 'E') throw new HlasmSyntax(`${macro} takes MF only as (E,addr)`);
32
+ if (sf && sf.form === 'L' && (mf || kw.has('PARAM'))) throw new HlasmSyntax(`the list form of ${macro} takes no MF or PARAM`);
33
+ const out = {};
34
+ if (sf) out.sf = sf.form === 'L' ? 'L' : { form: 'E', list: sf.list };
35
+ if (mf) out.mf = { form: 'E', list: mf.list };
36
+ return { out, entryOptional: Boolean(sf) };
37
+ }
38
+
39
+ // PARAM=(addr,addr,...): an execute form may leave an entry empty, to keep the list form's.
40
+ // A list form (SF=L) may write a keyword with no value, reserving its field for the execute form.
41
+ function written(keywords) {
42
+ if (!/^L$/i.test(String(keywords.get('SF') || '').trim())) return keywords;
43
+ return new Map([...keywords].filter(([, v]) => String(v).trim() !== ''));
44
+ }
45
+
46
+ function parseParam(raw, execute = false) {
47
+ const p = raw.trim();
48
+ if (p.startsWith('(') && p.endsWith(')')) {
49
+ const inner = p.slice(1, -1).trim();
50
+ if (inner !== '') {
51
+ for (const part of splitOperands(inner).map(s => s.trim())) {
52
+ if (part === '' ? !execute : !isAddress(part)) throw new HlasmSyntax('invalid PARAM address');
53
+ }
54
+ }
55
+ } else if (!isAddress(p)) {
56
+ throw new HlasmSyntax('invalid PARAM address');
57
+ }
58
+ return p;
59
+ }
60
+
9
61
  function parseLink(st, ops) {
10
- const kw = ops.keywords;
11
- const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'PARAM', 'VL', 'ID', 'ERRET', 'LSEARCH']);
62
+ const kw = written(ops.keywords);
63
+ const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'PARAM', 'VL', 'ID', 'ERRET', 'LSEARCH', 'SF', 'MF']);
12
64
  for (const k of kw.keys()) if (!allowed.has(k)) throw new HlasmSyntax(`unknown keyword ${k}`);
13
- if (ops.positional.length > 0) throw new HlasmSyntax('unexpected positional operand');
65
+ if (ops.positional.some((p) => p.trim() !== '')) throw new HlasmSyntax('unexpected positional operand');
66
+ const { out: form, entryOptional } = forms(kw, 'LINK');
14
67
  const epKeys = ['EP', 'EPLOC', 'DE'].filter(k => kw.has(k));
15
68
  if (epKeys.length > 1) throw new HlasmSyntax('EP, EPLOC, and DE are mutually exclusive');
16
- if (epKeys.length === 0) throw new HlasmSyntax('missing EP, EPLOC, or DE');
69
+ if (epKeys.length === 0 && !entryOptional) throw new HlasmSyntax('missing EP, EPLOC, or DE');
17
70
  const ep = kw.has('EP') ? kw.get('EP').trim().toUpperCase() : null;
18
71
  const eploc = kw.has('EPLOC') ? kw.get('EPLOC') : null;
19
72
  const de = kw.has('DE') ? kw.get('DE') : null;
20
73
  if (ep && !NAME.test(ep)) throw new HlasmSyntax('invalid EP name');
21
- if (eploc && !ADDR.test(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
22
- if (de && !ADDR.test(de.trim())) throw new HlasmSyntax('invalid DE address');
23
- if (kw.has('DCB') && !ADDR.test(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
24
- if (kw.has('ERRET') && !ADDR.test(kw.get('ERRET').trim())) throw new HlasmSyntax('invalid ERRET address');
74
+ if (eploc && !isAddress(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
75
+ if (de && !isAddress(de.trim())) throw new HlasmSyntax('invalid DE address');
76
+ if (kw.has('DCB') && !isAddress(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
77
+ if (kw.has('ERRET') && !isAddress(kw.get('ERRET').trim())) throw new HlasmSyntax('invalid ERRET address');
25
78
  if (kw.has('LSEARCH') && !YES_NO.test(kw.get('LSEARCH').trim())) throw new HlasmSyntax('invalid LSEARCH value');
26
79
  if (kw.has('VL') && !/^\d+$/.test(kw.get('VL').trim())) throw new HlasmSyntax('invalid VL value');
27
80
  if (kw.has('ID') && !/^\d+$/.test(kw.get('ID').trim())) throw new HlasmSyntax('invalid ID value');
28
- let param = null;
29
- if (kw.has('PARAM')) {
30
- const p = kw.get('PARAM').trim();
31
- if (p.startsWith('(') && p.endsWith(')')) {
32
- const inner = p.slice(1, -1).trim();
33
- if (inner !== '') {
34
- const parts = inner.split(',').map(s => s.trim());
35
- for (const part of parts) {
36
- if (!ADDR.test(part)) throw new HlasmSyntax('invalid PARAM address');
37
- }
38
- }
39
- } else if (!ADDR.test(p)) {
40
- throw new HlasmSyntax('invalid PARAM address');
41
- }
42
- param = p;
43
- }
44
- return { kind: 'LINK', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, param, keywords: Object.fromEntries(kw) };
81
+ const param = kw.has('PARAM') ? parseParam(kw.get('PARAM'), Boolean(form.mf)) : null;
82
+ return { kind: 'LINK', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, param, ...form, keywords: Object.fromEntries(kw) };
45
83
  }
46
84
 
47
85
  function parseXctl(st, ops) {
48
- const kw = ops.keywords;
49
- const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'LSEARCH']);
86
+ const kw = written(ops.keywords);
87
+ const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'LSEARCH', 'SF', 'MF', 'PARAM', 'VL']);
50
88
  for (const k of kw.keys()) if (!allowed.has(k)) throw new HlasmSyntax(`unknown keyword ${k}`);
89
+ const { out: form, entryOptional } = forms(kw, 'XCTL');
90
+ // XCTL builds a parameter list only in its execute form, at MF=(E,addr).
91
+ if ((kw.has('PARAM') || kw.has('VL')) && !form.mf) throw new HlasmSyntax('XCTL takes PARAM and VL only with MF=(E,addr)');
92
+ if (kw.has('VL') && !/^\d+$/.test(kw.get('VL').trim())) throw new HlasmSyntax('invalid VL value');
51
93
  // The registers to restore before control passes: (reg) or (reg1,reg2), numbers or EQU symbols.
52
94
  if (ops.positional.length > 1) throw new HlasmSyntax('too many positional operands');
53
95
  if (ops.positional.length === 1 && ops.positional[0].trim() !== '' && !/^\(\s*[A-Z0-9$#@_]+\s*(,\s*[A-Z0-9$#@_]+\s*)?\)$/i.test(ops.positional[0].trim())) throw new HlasmSyntax('invalid register list');
54
96
  const epKeys = ['EP', 'EPLOC', 'DE'].filter(k => kw.has(k));
55
97
  if (epKeys.length > 1) throw new HlasmSyntax('EP, EPLOC, and DE are mutually exclusive');
56
- if (epKeys.length === 0) throw new HlasmSyntax('missing EP, EPLOC, or DE');
98
+ if (epKeys.length === 0 && !entryOptional) throw new HlasmSyntax('missing EP, EPLOC, or DE');
57
99
  const ep = kw.has('EP') ? kw.get('EP').trim().toUpperCase() : null;
58
100
  const eploc = kw.has('EPLOC') ? kw.get('EPLOC') : null;
59
101
  const de = kw.has('DE') ? kw.get('DE') : null;
60
102
  if (ep && !NAME.test(ep)) throw new HlasmSyntax('invalid EP name');
61
- if (eploc && !ADDR.test(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
62
- if (de && !ADDR.test(de.trim())) throw new HlasmSyntax('invalid DE address');
63
- if (kw.has('DCB') && !ADDR.test(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
103
+ if (eploc && !isAddress(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
104
+ if (de && !isAddress(de.trim())) throw new HlasmSyntax('invalid DE address');
105
+ if (kw.has('DCB') && !isAddress(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
64
106
  if (kw.has('LSEARCH') && !YES_NO.test(kw.get('LSEARCH').trim())) throw new HlasmSyntax('invalid LSEARCH value');
65
- return { kind: 'XCTL', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, keywords: Object.fromEntries(kw) };
107
+ const param = kw.has('PARAM') ? parseParam(kw.get('PARAM'), true) : null;
108
+ return { kind: 'XCTL', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, ...(param ? { param } : {}), ...form, keywords: Object.fromEntries(kw) };
66
109
  }
67
110
 
68
111
  function parseLoad(st, ops) {
69
- const kw = ops.keywords;
70
- const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'ERRET', 'LSEARCH', 'LOADPT', 'LOADPT64', 'EXTINFO', 'RELATED']);
112
+ const kw = written(ops.keywords);
113
+ // The standard, list and execute forms, authorized keywords included (z/OS 3.1 MVS Programming:
114
+ // Authorized Assembler Services Reference, Volume 3, LOAD).
115
+ const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'ERRET', 'LSEARCH', 'LOADPT', 'LOADPT64', 'EXTINFO', 'RELATED',
116
+ 'ADDR', 'ADDR64', 'ADRNAPF', 'ADRNAPF64', 'ARCHLVL', 'EOM', 'FETCHOPT', 'GLOBAL', 'PLISTVER', 'RMODE', 'SF']);
71
117
  for (const k of kw.keys()) if (!allowed.has(k)) throw new HlasmSyntax(`unknown keyword ${k}`);
72
- if (ops.positional.length > 0) throw new HlasmSyntax('unexpected positional operand');
118
+ if (ops.positional.some((p) => p.trim() !== '')) throw new HlasmSyntax('unexpected positional operand');
119
+ const { out: form, entryOptional } = forms(kw, 'LOAD');
73
120
  const epKeys = ['EP', 'EPLOC', 'DE'].filter(k => kw.has(k));
74
121
  if (epKeys.length > 1) throw new HlasmSyntax('EP, EPLOC, and DE are mutually exclusive');
75
- if (epKeys.length === 0) throw new HlasmSyntax('missing EP, EPLOC, or DE');
122
+ if (epKeys.length === 0 && !entryOptional) throw new HlasmSyntax('missing EP, EPLOC, or DE');
76
123
  const ep = kw.has('EP') ? kw.get('EP').trim().toUpperCase() : null;
77
124
  const eploc = kw.has('EPLOC') ? kw.get('EPLOC') : null;
78
125
  const de = kw.has('DE') ? kw.get('DE') : null;
79
126
  if (ep && !NAME.test(ep)) throw new HlasmSyntax('invalid EP name');
80
- if (eploc && !ADDR.test(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
81
- if (de && !ADDR.test(de.trim())) throw new HlasmSyntax('invalid DE address');
82
- if (kw.has('DCB') && !ADDR.test(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
83
- if (kw.has('ERRET') && !ADDR.test(kw.get('ERRET').trim())) throw new HlasmSyntax('invalid ERRET address');
127
+ if (eploc && !isAddress(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
128
+ if (de && !isAddress(de.trim())) throw new HlasmSyntax('invalid DE address');
129
+ if (kw.has('DCB') && !isAddress(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
130
+ if (kw.has('ERRET') && !isAddress(kw.get('ERRET').trim())) throw new HlasmSyntax('invalid ERRET address');
84
131
  if (kw.has('LSEARCH') && !YES_NO.test(kw.get('LSEARCH').trim())) throw new HlasmSyntax('invalid LSEARCH value');
85
- if (kw.has('LOADPT') && !ADDR.test(kw.get('LOADPT').trim())) throw new HlasmSyntax('invalid LOADPT address');
86
- if (kw.has('LOADPT64') && !ADDR.test(kw.get('LOADPT64').trim())) throw new HlasmSyntax('invalid LOADPT64 address');
87
- if (kw.has('EXTINFO') && !ADDR.test(kw.get('EXTINFO').trim())) throw new HlasmSyntax('invalid EXTINFO address');
88
- return { kind: 'LOAD', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, keywords: Object.fromEntries(kw) };
132
+ if (kw.has('LOADPT') && !isAddress(kw.get('LOADPT').trim())) throw new HlasmSyntax('invalid LOADPT address');
133
+ if (kw.has('LOADPT64') && !isAddress(kw.get('LOADPT64').trim())) throw new HlasmSyntax('invalid LOADPT64 address');
134
+ if (kw.has('EXTINFO') && !isAddress(kw.get('EXTINFO').trim())) throw new HlasmSyntax('invalid EXTINFO address');
135
+ return { kind: 'LOAD', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, ...form, keywords: Object.fromEntries(kw) };
89
136
  }
90
137
 
91
- function parseAttach(st, ops) {
92
- const kw = ops.keywords;
93
- const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'LPMOD', 'DPMOD', 'PARAM', 'VL', 'ECB', 'ETXR', 'GSPV', 'GSPL', 'SHSPV', 'SHSPL', 'SZERO', 'TASKLIB', 'STAI', 'ESTAI', 'PURGE', 'ASYNCH', 'TERM', 'ALCOPY', 'RELATED']);
94
- for (const k of kw.keys()) if (!allowed.has(k)) throw new HlasmSyntax(`unknown keyword ${k}`);
95
- if (ops.positional.length > 0) throw new HlasmSyntax('unexpected positional operand');
138
+ function parseAttach(st, ops, opts) {
139
+ const kw = written(ops.keywords);
140
+ // The standard, list and execute forms, authorized keywords included (z/OS 3.1 MVS Programming:
141
+ // Authorized Assembler Services Reference, Volume 1, ATTACH and ATTACHX).
142
+ const allowed = new Set(['EP', 'EPLOC', 'DE', 'DCB', 'LPMOD', 'DPMOD', 'PARAM', 'VL', 'ECB', 'ETXR', 'GSPV', 'GSPL', 'SHSPV', 'SHSPL', 'SZERO', 'TASKLIB', 'STAI', 'ESTAI', 'PURGE', 'ASYNCH', 'TERM', 'ALCOPY', 'RELATED',
143
+ 'ADDRENV', 'ASCENV', 'BYADDRESS', 'DISP', 'JSTCB', 'KEY', 'MF', 'NSHSPL', 'NSHSPV', 'PKM', 'PLIST4', 'PLIST8', 'PLIST8ARALETS', 'PLISTARALETS', 'RSAPF', 'SDWALOC31', 'SF', 'SM', 'SVAREA', 'TCB', 'TID']);
144
+ const legacy = {};
145
+ for (const k of kw.keys()) {
146
+ if (MVS38_KEYWORDS.ATTACH.has(k)) mvs38Form(opts, legacy, 'ATTACH', `${k}=${kw.get(k).trim()}`);
147
+ else if (!allowed.has(k)) throw new HlasmSyntax(`unknown keyword ${k}`);
148
+ }
149
+ if (kw.has('HIARCHY') && !MVS38_HIARCHY.test(kw.get('HIARCHY').trim())) throw new HlasmSyntax('invalid HIARCHY value');
150
+ if (kw.has('JSCB') && !isAddress(kw.get('JSCB').trim())) throw new HlasmSyntax('invalid JSCB address');
151
+ if (ops.positional.some((p) => p.trim() !== '')) throw new HlasmSyntax('unexpected positional operand');
152
+ const { out: form, entryOptional } = forms(kw, 'ATTACH');
96
153
  const epKeys = ['EP', 'EPLOC', 'DE'].filter(k => kw.has(k));
97
154
  if (epKeys.length > 1) throw new HlasmSyntax('EP, EPLOC, and DE are mutually exclusive');
98
- if (epKeys.length === 0) throw new HlasmSyntax('missing EP, EPLOC, or DE');
155
+ if (epKeys.length === 0 && !entryOptional) throw new HlasmSyntax('missing EP, EPLOC, or DE');
99
156
  const ep = kw.has('EP') ? kw.get('EP').trim().toUpperCase() : null;
100
157
  const eploc = kw.has('EPLOC') ? kw.get('EPLOC') : null;
101
158
  const de = kw.has('DE') ? kw.get('DE') : null;
102
159
  if (ep && !NAME.test(ep)) throw new HlasmSyntax('invalid EP name');
103
- if (eploc && !ADDR.test(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
104
- if (de && !ADDR.test(de.trim())) throw new HlasmSyntax('invalid DE address');
105
- if (kw.has('DCB') && !ADDR.test(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
106
- if (kw.has('LPMOD') && !NUM_OR_ADDR.test(kw.get('LPMOD').trim())) throw new HlasmSyntax('invalid LPMOD value');
107
- if (kw.has('DPMOD') && !NUM_OR_ADDR.test(kw.get('DPMOD').trim())) throw new HlasmSyntax('invalid DPMOD value');
160
+ if (eploc && !isAddress(eploc.trim())) throw new HlasmSyntax('invalid EPLOC address');
161
+ if (de && !isAddress(de.trim())) throw new HlasmSyntax('invalid DE address');
162
+ if (kw.has('DCB') && !isAddress(kw.get('DCB').trim())) throw new HlasmSyntax('invalid DCB address');
163
+ if (kw.has('LPMOD') && !isAddress(kw.get('LPMOD').trim())) throw new HlasmSyntax('invalid LPMOD value');
164
+ if (kw.has('DPMOD') && !isAddress(kw.get('DPMOD').trim())) throw new HlasmSyntax('invalid DPMOD value');
108
165
  if (kw.has('VL') && !/^\d+$/.test(kw.get('VL').trim())) throw new HlasmSyntax('invalid VL value');
109
- if (kw.has('ECB') && !ADDR.test(kw.get('ECB').trim())) throw new HlasmSyntax('invalid ECB address');
110
- if (kw.has('ETXR') && !ADDR.test(kw.get('ETXR').trim())) throw new HlasmSyntax('invalid ETXR address');
111
- if (kw.has('GSPV') && !NUM_OR_ADDR.test(kw.get('GSPV').trim())) throw new HlasmSyntax('invalid GSPV value');
112
- if (kw.has('GSPL') && !ADDR.test(kw.get('GSPL').trim())) throw new HlasmSyntax('invalid GSPL address');
113
- if (kw.has('SHSPV') && !NUM_OR_ADDR.test(kw.get('SHSPV').trim())) throw new HlasmSyntax('invalid SHSPV value');
114
- if (kw.has('SHSPL') && !ADDR.test(kw.get('SHSPL').trim())) throw new HlasmSyntax('invalid SHSPL address');
166
+ if (kw.has('ECB') && !isAddress(kw.get('ECB').trim())) throw new HlasmSyntax('invalid ECB address');
167
+ if (kw.has('ETXR') && !isAddress(kw.get('ETXR').trim())) throw new HlasmSyntax('invalid ETXR address');
168
+ if (kw.has('GSPV') && !isAddress(kw.get('GSPV').trim())) throw new HlasmSyntax('invalid GSPV value');
169
+ if (kw.has('GSPL') && !isAddress(kw.get('GSPL').trim())) throw new HlasmSyntax('invalid GSPL address');
170
+ if (kw.has('SHSPV') && !isAddress(kw.get('SHSPV').trim())) throw new HlasmSyntax('invalid SHSPV value');
171
+ if (kw.has('SHSPL') && !isAddress(kw.get('SHSPL').trim())) throw new HlasmSyntax('invalid SHSPL address');
115
172
  if (kw.has('SZERO') && !YES_NO.test(kw.get('SZERO').trim())) throw new HlasmSyntax('invalid SZERO value');
116
- if (kw.has('TASKLIB') && !ADDR.test(kw.get('TASKLIB').trim())) throw new HlasmSyntax('invalid TASKLIB address');
117
- if (kw.has('STAI') && !ADDR.test(kw.get('STAI').trim())) throw new HlasmSyntax('invalid STAI address');
118
- if (kw.has('ESTAI') && !ADDR.test(kw.get('ESTAI').trim())) throw new HlasmSyntax('invalid ESTAI address');
173
+ if (kw.has('TASKLIB') && !isAddress(kw.get('TASKLIB').trim())) throw new HlasmSyntax('invalid TASKLIB address');
174
+ if (kw.has('STAI') && !isExit(kw.get('STAI'))) throw new HlasmSyntax('invalid STAI address');
175
+ if (kw.has('ESTAI') && !isExit(kw.get('ESTAI'))) throw new HlasmSyntax('invalid ESTAI address');
119
176
  if (kw.has('PURGE') && !/^(?:QUIESCE|NONE|HALT)$/i.test(kw.get('PURGE').trim())) throw new HlasmSyntax('invalid PURGE value');
120
177
  if (kw.has('ASYNCH') && !YES_NO.test(kw.get('ASYNCH').trim())) throw new HlasmSyntax('invalid ASYNCH value');
121
178
  if (kw.has('TERM') && !YES_NO.test(kw.get('TERM').trim())) throw new HlasmSyntax('invalid TERM value');
122
179
  if (kw.has('ALCOPY') && !YES_NO.test(kw.get('ALCOPY').trim())) throw new HlasmSyntax('invalid ALCOPY value');
123
- let param = null;
124
- if (kw.has('PARAM')) {
125
- const p = kw.get('PARAM').trim();
126
- if (p.startsWith('(') && p.endsWith(')')) {
127
- const inner = p.slice(1, -1).trim();
128
- if (inner !== '') {
129
- const parts = inner.split(',').map(s => s.trim());
130
- for (const part of parts) {
131
- if (!ADDR.test(part)) throw new HlasmSyntax('invalid PARAM address');
132
- }
133
- }
134
- } else if (!ADDR.test(p)) {
135
- throw new HlasmSyntax('invalid PARAM address');
136
- }
137
- param = p;
138
- }
139
- return { kind: 'ATTACH', ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, param, keywords: Object.fromEntries(kw) };
180
+ const param = kw.has('PARAM') ? parseParam(kw.get('PARAM'), Boolean(form.mf)) : null;
181
+ return { kind: 'ATTACH', ...legacy, ep, eploc, de, dcb: kw.has('DCB') ? kw.get('DCB') : null, param, ...form, keywords: Object.fromEntries(kw) };
140
182
  }
141
183
 
142
184
  export const parsers = Object.freeze({ LINK: parseLink, XCTL: parseXctl, LOAD: parseLoad, ATTACH: parseAttach });
@@ -2,7 +2,10 @@
2
2
  // Parsers for the z/OS recovery and dump macros: ESTAE, ESPIE, ABEND, SNAP.
3
3
  import { HlasmSyntax } from '../operands.mjs';
4
4
 
5
- const ESTAE_KW = new Set(['PARAM', 'XCTL', 'PURGE', 'ASYNCH', 'TERM', 'ASCENV', 'RECORD', 'RELATED', 'MF']);
5
+ // The standard, list and execute forms, authorized keywords included (z/OS 3.1 MVS Programming:
6
+ // Authorized Assembler Services Reference, Volume 2, ESTAE and ESTAEX).
7
+ const ESTAE_KW = new Set(['PARAM', 'XCTL', 'PURGE', 'ASYNCH', 'TERM', 'ASCENV', 'RECORD', 'RELATED', 'MF',
8
+ 'AMODE64', 'BRANCH', 'CANCEL', 'KEY', 'SDWALOC31', 'SPIEOVERRIDE', 'SVEAREA', 'TKNPASS', 'TOKEN']);
6
9
  const ESPIE_KW = new Set(['PKM', 'MF']);
7
10
  const ABEND_KW = new Set(['REASON', 'DUMPOPT', 'DUMPOPX', 'MF']);
8
11
  const SNAP_KW = new Set(['DCB', 'TCB', 'ID', 'SDATA', 'PDATA', 'STORAGE', 'LIST', 'STRHDR', 'SUBPLST', 'DSPSTOR', 'MF']);
@@ -16,13 +19,15 @@ function checkKw(kw, allowed, op) {
16
19
  function parseEstae(st, ops) {
17
20
  const { positional, keywords } = ops;
18
21
  checkKw(keywords, ESTAE_KW, 'ESTAE');
19
- if (positional.length < 1 || positional.length > 3) {
22
+ // The list and execute forms (MF=) may leave the exit and the CT or OV to the other form.
23
+ const remote = keywords.has('MF');
24
+ if ((positional.length < 1 && !remote) || positional.length > 3) {
20
25
  throw new HlasmSyntax('ESTAE: expected 1 to 3 positional operands');
21
26
  }
22
- const exit = positional[0];
27
+ const exit = positional[0] ?? '';
23
28
  for (let i = 1; i < positional.length; i++) {
24
- const p = positional[i].toUpperCase();
25
- if (p !== 'CT' && p !== 'OV') {
29
+ const p = positional[i].trim().toUpperCase();
30
+ if (p !== 'CT' && p !== 'OV' && !(remote && p === '')) {
26
31
  throw new HlasmSyntax(`ESTAE: unexpected positional operand ${positional[i]}`);
27
32
  }
28
33
  }
@@ -1,9 +1,41 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Parser for z/OS virtual storage macros: STORAGE, GETMAIN, and FREEMAIN.
3
- import { HlasmSyntax } from '../operands.mjs';
3
+ import { HlasmSyntax, remoteForm } from '../operands.mjs';
4
+ import { MVS38_HIARCHY, MVS38_KEYWORDS, MVS38_STORAGE_MODES, mvs38Form } from '../mvs38.mjs';
4
5
 
5
6
  const GETMAIN_MODES = new Set(['R', 'RU', 'RC', 'VU', 'VC', 'EU', 'EC', 'LU', 'LC', 'VRC', 'VRU']);
6
7
  const FREEMAIN_MODES = new Set(['R', 'RU', 'RC', 'VU', 'VC', 'V', 'EU', 'EC', 'E', 'LU', 'LC', 'L']);
8
+ // The request types that have list and execute forms (MF=L, MF=(E,addr)), and those that take
9
+ // BRANCH=(YES,GLOBAL) (z/OS 3.1 MVS Programming: Assembler Services Reference, Volume 1, and
10
+ // Authorized Assembler Services Reference, Volume 2, GETMAIN and FREEMAIN). The request type is
11
+ // optional in both forms: the execute form takes it from the list it names.
12
+ const GETMAIN_LIST_MODES = new Set(['LC', 'LU', 'VC', 'VU', 'EC', 'EU']);
13
+ const FREEMAIN_LIST_MODES = new Set(['LC', 'LU', 'L', 'VC', 'VU', 'V', 'EC', 'EU', 'E']);
14
+ const GETMAIN_GLOBAL_MODES = new Set(['RC', 'RU', 'VRC', 'VRU']);
15
+ const FREEMAIN_GLOBAL_MODES = new Set(['RC', 'RU']);
16
+ const OWNERS = new Set(['HOME', 'PRIMARY', 'SECONDARY', 'SYSTEM']);
17
+
18
+ // MF=L or MF=(E,addr), and BRANCH=YES or (YES,GLOBAL), each checked against the request type.
19
+ function remoteAndBranch(macro, mode, keywords, listModes, globalModes) {
20
+ const out = {};
21
+ if (keywords.has('MF')) {
22
+ const { form, list } = remoteForm(keywords.get('MF'), 'MF');
23
+ if (mode && !listModes.has(mode)) throw new HlasmSyntax(`${macro} ${mode} has no list or execute form`);
24
+ out.mf = form === 'L' ? 'L' : { form: 'E', list };
25
+ }
26
+ if (keywords.has('BRANCH')) {
27
+ const v = keywords.get('BRANCH').replace(/\s+/g, '').toUpperCase();
28
+ if (v === '(YES,GLOBAL)') {
29
+ if (!globalModes.has(mode)) throw new HlasmSyntax(`${macro} takes BRANCH=(YES,GLOBAL) only with ${[...globalModes].join(', ')}`);
30
+ out.branch = 'GLOBAL';
31
+ } else if (v === 'YES' || v === '(YES)') {
32
+ out.branch = 'YES';
33
+ } else {
34
+ throw new HlasmSyntax(`${macro} BRANCH= value ${keywords.get('BRANCH')} is not YES or (YES,GLOBAL)`);
35
+ }
36
+ }
37
+ return out;
38
+ }
7
39
 
8
40
  function getKeyword(keywords, key) {
9
41
  const val = keywords.get(key);
@@ -42,25 +74,35 @@ function parseStorage(st, ops) {
42
74
  };
43
75
  }
44
76
 
45
- function parseGetmain(st, ops) {
77
+ function parseGetmain(st, ops, opts) {
46
78
  const { positional, keywords } = ops;
47
- if (positional.length < 1) {
79
+ const mode = (positional[0] ?? '').trim().toUpperCase() || null;
80
+ if (!mode && !keywords.has('MF')) {
48
81
  throw new HlasmSyntax('GETMAIN requires a request type');
49
82
  }
50
- const mode = positional[0].trim().toUpperCase();
51
- if (!GETMAIN_MODES.has(mode)) {
83
+ const legacy = {};
84
+ if (mode && MVS38_STORAGE_MODES.has(mode)) {
85
+ mvs38Form(opts, legacy, 'GETMAIN', mode);
86
+ } else if (mode && !GETMAIN_MODES.has(mode)) {
52
87
  throw new HlasmSyntax(`GETMAIN request type must be one of ${[...GETMAIN_MODES].join(', ')}, got ${positional[0]}`);
53
88
  }
54
- if (positional.length > 1) {
89
+ if (positional.slice(1).some((p) => p.trim() !== '')) {
55
90
  throw new HlasmSyntax(`GETMAIN does not accept extra positional operands: ${positional[1]}`);
56
91
  }
57
92
 
58
- const allowed = new Set(['LA', 'LV', 'A', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'LOC', 'INADDR', 'CHECKZERO', 'RELATED']);
93
+ const allowed = new Set(['LA', 'LV', 'A', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'LOC', 'INADDR', 'CHECKZERO', 'RELATED', 'MF', 'BRANCH', 'OWNER']);
59
94
  for (const key of keywords.keys()) {
60
- if (!allowed.has(key)) {
95
+ if (MVS38_KEYWORDS.GETMAIN.has(key)) {
96
+ mvs38Form(opts, legacy, 'GETMAIN', `${key}=${keywords.get(key).trim()}`);
97
+ if (!MVS38_HIARCHY.test(keywords.get(key).trim())) throw new HlasmSyntax('GETMAIN HIARCHY must be 0 or 1');
98
+ } else if (!allowed.has(key)) {
61
99
  throw new HlasmSyntax(`GETMAIN does not accept keyword ${key}`);
62
100
  }
63
101
  }
102
+ if (keywords.has('OWNER') && !OWNERS.has(keywords.get('OWNER').trim().toUpperCase())) {
103
+ throw new HlasmSyntax(`GETMAIN OWNER must be one of ${[...OWNERS].join(', ')}`);
104
+ }
105
+ const extra = remoteAndBranch('GETMAIN', mode, keywords, GETMAIN_LIST_MODES, GETMAIN_GLOBAL_MODES);
64
106
 
65
107
  const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
66
108
  const address = getKeyword(keywords, 'A');
@@ -68,33 +110,39 @@ function parseGetmain(st, ops) {
68
110
 
69
111
  return {
70
112
  kind: 'GETMAIN',
113
+ ...legacy,
71
114
  mode,
72
115
  length,
73
116
  address,
74
117
  subpool,
118
+ ...extra,
75
119
  keywords: Object.fromEntries(keywords)
76
120
  };
77
121
  }
78
122
 
79
- function parseFreemain(st, ops) {
123
+ function parseFreemain(st, ops, opts) {
80
124
  const { positional, keywords } = ops;
81
- if (positional.length < 1) {
125
+ const mode = (positional[0] ?? '').trim().toUpperCase() || null;
126
+ if (!mode && !keywords.has('MF')) {
82
127
  throw new HlasmSyntax('FREEMAIN requires a request type');
83
128
  }
84
- const mode = positional[0].trim().toUpperCase();
85
- if (!FREEMAIN_MODES.has(mode)) {
129
+ const legacy = {};
130
+ if (mode && MVS38_STORAGE_MODES.has(mode)) {
131
+ mvs38Form(opts, legacy, 'FREEMAIN', mode);
132
+ } else if (mode && !FREEMAIN_MODES.has(mode)) {
86
133
  throw new HlasmSyntax(`FREEMAIN request type must be one of ${[...FREEMAIN_MODES].join(', ')}, got ${positional[0]}`);
87
134
  }
88
- if (positional.length > 1) {
135
+ if (positional.slice(1).some((p) => p.trim() !== '')) {
89
136
  throw new HlasmSyntax(`FREEMAIN does not accept extra positional operands: ${positional[1]}`);
90
137
  }
91
138
 
92
- const allowed = new Set(['LA', 'LV', 'A', 'SP', 'KEY', 'RELATED']);
139
+ const allowed = new Set(['LA', 'LV', 'A', 'SP', 'KEY', 'RELATED', 'MF', 'BRANCH']);
93
140
  for (const key of keywords.keys()) {
94
141
  if (!allowed.has(key)) {
95
142
  throw new HlasmSyntax(`FREEMAIN does not accept keyword ${key}`);
96
143
  }
97
144
  }
145
+ const extra = remoteAndBranch('FREEMAIN', mode, keywords, FREEMAIN_LIST_MODES, FREEMAIN_GLOBAL_MODES);
98
146
 
99
147
  const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
100
148
  const address = getKeyword(keywords, 'A');
@@ -102,10 +150,12 @@ function parseFreemain(st, ops) {
102
150
 
103
151
  return {
104
152
  kind: 'FREEMAIN',
153
+ ...legacy,
105
154
  mode,
106
155
  length,
107
156
  address,
108
157
  subpool,
158
+ ...extra,
109
159
  keywords: Object.fromEntries(keywords)
110
160
  };
111
161
  }
@@ -110,7 +110,7 @@ function parseWHEN(st, ops) {
110
110
  }
111
111
 
112
112
  function parseNoOperand(st, ops) {
113
- if (ops.positional.length > 0) throw new HlasmSyntax(`${st.operation} does not accept positional operands`);
113
+ if (ops.positional.some((p) => p.trim() !== '')) throw new HlasmSyntax(`${st.operation} does not accept positional operands`);
114
114
  return { kind: st.operation, keywords: Object.fromEntries(ops.keywords) };
115
115
  }
116
116
 
@@ -14,7 +14,8 @@ const FIXED_LENGTH = { '&SYSDATE': 8, '&SYSDATC': 8, '&SYSTIME': 5 };
14
14
  // &NAME, with an optional subscript (&A(1), &A(&I+1)) and an optional period that joins it to what
15
15
  // follows. && is one ampersand, not a variable symbol. Inside a quoted string a variable becomes as
16
16
  // many X as its value has characters when that is fixed, and one X otherwise, counted as unsized.
17
- function substitute(text) {
17
+ // Outside one it becomes what `standIn` gives for the nth such variable.
18
+ function substitute(text, standIn = (n) => `VAR${n}`) {
18
19
  let out = '';
19
20
  let quoted = false;
20
21
  let n = 0;
@@ -35,20 +36,44 @@ function substitute(text) {
35
36
  i--;
36
37
  const fixed = FIXED_LENGTH[m[0].toUpperCase()];
37
38
  if (quoted && !fixed) unsized++;
38
- out += quoted ? 'X'.repeat(fixed || 1) : `VAR${++n}`;
39
+ out += quoted ? 'X'.repeat(fixed || 1) : standIn(++n);
39
40
  }
40
- return { text: out, unsized };
41
+ return { text: out, unsized, variables: n };
42
+ }
43
+
44
+ // A variable outside quotes stands in for a symbol. When no statement reads that way, one variable
45
+ // at a time stands in for a quoted string (DC C&MSG, LA &R,=C&LIST), a number (DC &T.F'0') or an
46
+ // option (PRINT &GEN, AMODE &MODE), as a caller's value may be, and then all of them do; past
47
+ // eight variables only the uniform readings are tried.
48
+ const SYMBOL = (n) => `VAR${n}`;
49
+ const VALUES = ["'X'", '1', 'ON', 'ANY'];
50
+ function* standIns(variables) {
51
+ yield SYMBOL;
52
+ if (variables <= 8) {
53
+ for (let j = 1; j <= variables; j++) for (const value of VALUES) yield (n) => (n === j ? value : SYMBOL(n));
54
+ }
55
+ for (const value of VALUES) yield () => value;
41
56
  }
42
57
 
43
58
  function read(kind, st) {
44
59
  if (/&[A-Z$#@_]/i.test(st.operation)) throw new HlasmSyntax(`the operation ${st.operation} is decided when the macro is expanded`);
45
- const name = st.name && !st.name.startsWith('&') && !st.name.startsWith('.') ? st.name : null;
46
- const { text, unsized } = substitute(st.field || '');
47
- const res = parseHlasmStatement({ ...st, name, field: text, inMacro: false, prototype: false });
48
- // exact: no quoted string holds a variable of unknown length, so the node's lengths are the source's.
49
- if (res.status === 'parsed') return { kind, as: res.kind, node: res.node, exact: unsized === 0 };
50
- if (res.status === 'unbuilt') throw new HlasmSyntax(`no parser yet for ${res.kind}`);
51
- throw new HlasmSyntax(res.reason || `${res.kind} ${res.status}`);
60
+ // A name built from variable symbols (&NAME, KFBR&SYSNDX) stands in as an ordinary symbol, as the
61
+ // operands do; one still holding an ampersand would send the statement back here.
62
+ const named = st.name && !st.name.startsWith('.') ? substitute(st.name).text : null;
63
+ const name = named && !named.includes('&') ? named : null;
64
+ const { variables } = substitute(st.field || '');
65
+ let first = null;
66
+ for (const standIn of standIns(variables)) {
67
+ const { text, unsized } = substitute(st.field || '', standIn);
68
+ const res = parseHlasmStatement({ ...st, name, field: text, inMacro: false, prototype: false });
69
+ // exact: the symbol reading, with no quoted string holding a variable of unknown length, so the
70
+ // node's lengths are the source's.
71
+ if (res.status === 'parsed') return { kind, as: res.kind, node: res.node, exact: standIn === SYMBOL && unsized === 0 };
72
+ first ||= res;
73
+ if (!variables) break;
74
+ }
75
+ if (first.status === 'unbuilt') throw new HlasmSyntax(`no parser yet for ${first.kind}`);
76
+ throw new HlasmSyntax(first.reason || `${first.kind} ${first.status}`);
52
77
  }
53
78
 
54
79
  function prototype(st) {
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Operands the MVS 3.8 system macros take that z/OS 3.1's documentation does not list, with the
3
+ // meaning those macros give them (MVS 3.8j SYS1.MACLIB, public domain: MODESET, GETMAIN, FREEMAIN,
4
+ // ATTACH, DCB). Read by default; parseHlasmStatement(st, { mvs38: false }) refuses them.
5
+ import { HlasmSyntax } from './operands.mjs';
6
+
7
+ // MODESET EXTKEY= names and the PSW key each sets.
8
+ export const MVS38_EXTKEYS = new Map([
9
+ ['SUPR', 0], ['RSM', 0], ['VSM', 0], ['SRM', 0],
10
+ ['SCHED', 1], ['JES', 1], ['HASP', 1],
11
+ ['DATAMGT', 5],
12
+ ['TCAM', 6], ['VTAM', 6],
13
+ ]);
14
+
15
+ // GETMAIN and FREEMAIN P: a request for a subpool, register 0 holding its number in the high byte.
16
+ export const MVS38_STORAGE_MODES = new Set(['P']);
17
+
18
+ // HIARCHY= chose a storage hierarchy (0 or 1); MVS/XA removed hierarchies.
19
+ export const MVS38_HIARCHY = /^[01]$/;
20
+
21
+ export const MVS38_KEYWORDS = {
22
+ GETMAIN: new Set(['HIARCHY']),
23
+ ATTACH: new Set(['HIARCHY', 'JSCB']),
24
+ DCB: new Set(['PGFX', 'AERR']),
25
+ };
26
+
27
+ export const mvs38Enabled = (opts) => opts?.mvs38 !== false;
28
+
29
+ export const systemKeyExtkeys = (opts) => (mvs38Enabled(opts) ? [...MVS38_EXTKEYS].filter(([, key]) => key > 0) : []);
30
+
31
+ export const keyZeroExtkeys = (opts) => (mvs38Enabled(opts) ? [...MVS38_EXTKEYS].filter(([, key]) => key === 0).map(([name]) => name) : []);
32
+
33
+ // Marks an MVS 3.8 form on the node it is read into, or refuses it when the option is off.
34
+ export function mvs38Form(opts, node, macro, form) {
35
+ if (!mvs38Enabled(opts)) throw new HlasmSyntax(`${macro}: ${form} is an MVS 3.8 form`);
36
+ (node.mvs38 ||= []).push(form);
37
+ }
38
+
39
+ // BTAM's READ and WRITE types (IHBRDWRT); BTAM is not in z/OS. READ T alone is its execute form.
40
+ export const MVS38_BTAM_TYPES = {
41
+ READ: new Set(['T', 'TB', 'TI', 'TT', 'TV', 'TP', 'TS', 'TQ', 'TC', 'TE', 'TM', 'TIR', 'TTR', 'TVR', 'TPR', 'TTA', 'TTL', 'TPL', 'TIQ', 'TRV', 'TCW', 'TBR', 'TMP', 'TBP', 'TQM']),
42
+ WRITE: new Set(['TB', 'TI', 'TT', 'TV', 'TA', 'TS', 'TN', 'TR', 'TL', 'TD', 'TQ', 'TW', 'TC', 'TIR', 'TTR', 'TVR', 'TLR', 'TIO', 'TIV', 'TCO', 'TTV', 'TUS', 'TIX', 'TVO', 'TTX', 'TSR', 'TIE', 'TTE', 'TRM', 'TSA', 'TSF', 'TIVX', 'TTVX', 'TIXR', 'TTXR']),
43
+ };
44
+
45
+ // VTAM's basic-mode READ and WRITE, given RPL=, and the RPL fields they may set; z/OS
46
+ // Communications Server keeps SEND and RECEIVE and has no READ or WRITE.
47
+ export const MVS38_VTAM_RPL_KEYWORDS = new Set(['RPL', 'ACB', 'AREA', 'AREALEN', 'RECLEN', 'AAREA', 'AAREALN', 'ARECLEN', 'ECB', 'EXIT', 'OPTCD', 'ARG', 'BRANCH']);
@@ -47,3 +47,13 @@ export function macroOperands(field) {
47
47
  }
48
48
  return { positional, keywords };
49
49
  }
50
+
51
+ // A macro's list form (MF=L, SF=L) builds only its parameter list; its execute form ((E,addr))
52
+ // runs the service on a list built elsewhere, at addr.
53
+ export function remoteForm(value, keyword) {
54
+ const t = String(value).trim();
55
+ if (/^L$/i.test(t)) return { form: 'L', list: null };
56
+ const m = /^\(\s*E\s*,\s*(\S.*?)\s*\)$/i.exec(t);
57
+ if (m) return { form: 'E', list: m[1] };
58
+ throw new HlasmSyntax(`${keyword}= value ${t} is not L or (E,addr)`);
59
+ }