@portll/cobolwork 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -26
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +56 -14
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +133 -53
- package/lib/capabilities.mjs +13 -11
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +17 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +77 -4
- package/lib/dataflow.mjs +402 -152
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +26 -7
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +44 -15
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +128 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +161 -0
- package/lib/hlasm/asm/symbols.mjs +78 -0
- package/lib/hlasm/exec.mjs +27 -0
- package/lib/hlasm/expr.mjs +167 -0
- package/lib/hlasm/instr.mjs +73 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +146 -0
- package/lib/hlasm/macro/datasets.mjs +107 -0
- package/lib/hlasm/macro/io.mjs +162 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +77 -0
- package/lib/hlasm/macro/program.mjs +184 -0
- package/lib/hlasm/macro/recovery.mjs +85 -0
- package/lib/hlasm/macro/storage.mjs +167 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +97 -0
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +59 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +130 -0
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +20 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +32 -23
- package/lib/kernel/shared-pass.mjs +163 -0
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/options.mjs +26 -4
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/policy.mjs +6 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +8 -4
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +44 -3
- package/lib/scan.mjs +28 -12
- package/lib/sets/abend.mjs +82 -8
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +20 -38
- package/lib/sets/compile.mjs +46 -18
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +32 -8
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +124 -8
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +11 -9
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +112 -0
- package/lib/sets/semantics.mjs +8 -3
- package/lib/sets/web.mjs +39 -23
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +520 -5
- package/rules/compliance-dora.json +509 -5
- package/rules/compliance-ffiec.json +505 -1
- package/rules/compliance-nist80053.json +557 -1
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.policy.schema.json +7 -0
- package/schema/cobolwork.site.schema.json +116 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Parsers for the z/OS recovery and dump macros: ESTAE, ESPIE, ABEND, SNAP.
|
|
3
|
+
import { HlasmSyntax } from '../operands.mjs';
|
|
4
|
+
|
|
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']);
|
|
9
|
+
const ESPIE_KW = new Set(['PKM', 'MF']);
|
|
10
|
+
const ABEND_KW = new Set(['REASON', 'DUMPOPT', 'DUMPOPX', 'MF']);
|
|
11
|
+
const SNAP_KW = new Set(['DCB', 'TCB', 'ID', 'SDATA', 'PDATA', 'STORAGE', 'LIST', 'STRHDR', 'SUBPLST', 'DSPSTOR', 'MF']);
|
|
12
|
+
|
|
13
|
+
function checkKw(kw, allowed, op) {
|
|
14
|
+
for (const k of kw.keys()) {
|
|
15
|
+
if (!allowed.has(k)) throw new HlasmSyntax(`${op}: unknown keyword ${k}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseEstae(st, ops) {
|
|
20
|
+
const { positional, keywords } = ops;
|
|
21
|
+
checkKw(keywords, ESTAE_KW, 'ESTAE');
|
|
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) {
|
|
25
|
+
throw new HlasmSyntax('ESTAE: expected 1 to 3 positional operands');
|
|
26
|
+
}
|
|
27
|
+
const exit = positional[0] ?? '';
|
|
28
|
+
for (let i = 1; i < positional.length; i++) {
|
|
29
|
+
const p = positional[i].trim().toUpperCase();
|
|
30
|
+
if (p !== 'CT' && p !== 'OV' && !(remote && p === '')) {
|
|
31
|
+
throw new HlasmSyntax(`ESTAE: unexpected positional operand ${positional[i]}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { kind: 'ESTAE', exit, keywords: Object.fromEntries(keywords) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseEspie(st, ops) {
|
|
38
|
+
const { positional, keywords } = ops;
|
|
39
|
+
checkKw(keywords, ESPIE_KW, 'ESPIE');
|
|
40
|
+
if (positional.length < 1) throw new HlasmSyntax('ESPIE: missing action');
|
|
41
|
+
const action = positional[0].toUpperCase();
|
|
42
|
+
if (action !== 'SET' && action !== 'RESET' && action !== 'TEST') {
|
|
43
|
+
throw new HlasmSyntax(`ESPIE: invalid action ${positional[0]}`);
|
|
44
|
+
}
|
|
45
|
+
const rest = positional.slice(1);
|
|
46
|
+
if (action === 'SET') {
|
|
47
|
+
if (rest.length > 2) throw new HlasmSyntax('ESPIE SET: too many operands');
|
|
48
|
+
} else if (rest.length > 0) {
|
|
49
|
+
throw new HlasmSyntax(`ESPIE ${action}: no operands expected`);
|
|
50
|
+
}
|
|
51
|
+
return { kind: 'ESPIE', action, operands: rest, keywords: Object.fromEntries(keywords) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseAbend(st, ops) {
|
|
55
|
+
const { positional, keywords } = ops;
|
|
56
|
+
checkKw(keywords, ABEND_KW, 'ABEND');
|
|
57
|
+
if (positional.length < 1 || positional.length > 4) {
|
|
58
|
+
throw new HlasmSyntax('ABEND: expected 1 to 4 positional operands');
|
|
59
|
+
}
|
|
60
|
+
const code = positional[0];
|
|
61
|
+
let dump = false, step = false;
|
|
62
|
+
for (let i = 1; i < positional.length; i++) {
|
|
63
|
+
const p = positional[i].toUpperCase();
|
|
64
|
+
if (p === '') continue;
|
|
65
|
+
if (p === 'DUMP') dump = true;
|
|
66
|
+
else if (p === 'STEP') step = true;
|
|
67
|
+
else if (p === 'USER' || p === 'SYSTEM') continue;
|
|
68
|
+
else throw new HlasmSyntax(`ABEND: unexpected positional operand ${positional[i]}`);
|
|
69
|
+
}
|
|
70
|
+
return { kind: 'ABEND', code, dump, step, keywords: Object.fromEntries(keywords) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseSnap(st, ops) {
|
|
74
|
+
const { keywords } = ops;
|
|
75
|
+
checkKw(keywords, SNAP_KW, 'SNAP');
|
|
76
|
+
const dcb = keywords.has('DCB') ? keywords.get('DCB') : null;
|
|
77
|
+
return { kind: 'SNAP', dcb, keywords: Object.fromEntries(keywords) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const parsers = {
|
|
81
|
+
ESTAE: parseEstae,
|
|
82
|
+
ESPIE: parseEspie,
|
|
83
|
+
ABEND: parseAbend,
|
|
84
|
+
SNAP: parseSnap
|
|
85
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Parser for z/OS virtual storage macros: STORAGE, GETMAIN, and FREEMAIN.
|
|
3
|
+
import { HlasmSyntax, remoteForm } from '../operands.mjs';
|
|
4
|
+
import { MVS38_HIARCHY, MVS38_KEYWORDS, MVS38_STORAGE_MODES, mvs38Form } from '../mvs38.mjs';
|
|
5
|
+
|
|
6
|
+
const GETMAIN_MODES = new Set(['R', 'RU', 'RC', 'VU', 'VC', 'EU', 'EC', 'LU', 'LC', 'VRC', 'VRU']);
|
|
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
|
+
}
|
|
39
|
+
|
|
40
|
+
function getKeyword(keywords, key) {
|
|
41
|
+
const val = keywords.get(key);
|
|
42
|
+
return val === undefined ? null : val;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseStorage(st, ops) {
|
|
46
|
+
const { positional, keywords } = ops;
|
|
47
|
+
if (positional.length < 1) {
|
|
48
|
+
throw new HlasmSyntax('STORAGE requires a request type (OBTAIN or RELEASE)');
|
|
49
|
+
}
|
|
50
|
+
const request = positional[0].trim().toUpperCase();
|
|
51
|
+
if (request !== 'OBTAIN' && request !== 'RELEASE') {
|
|
52
|
+
throw new HlasmSyntax(`STORAGE request must be OBTAIN or RELEASE, got ${positional[0]}`);
|
|
53
|
+
}
|
|
54
|
+
if (positional.length > 1) {
|
|
55
|
+
throw new HlasmSyntax(`STORAGE does not accept extra positional operands: ${positional[1]}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const allowed = new Set(['LENGTH', 'ADDR', 'INADDR', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'CALLRKY', 'LOC', 'LINKAGE', 'RTCD', 'COND', 'CHECKZERO', 'BACK', 'FIX', 'EXECUTABLE', 'RELATED']);
|
|
59
|
+
for (const key of keywords.keys()) {
|
|
60
|
+
if (!allowed.has(key)) {
|
|
61
|
+
throw new HlasmSyntax(`STORAGE does not accept keyword ${key}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
kind: 'STORAGE',
|
|
67
|
+
request,
|
|
68
|
+
length: getKeyword(keywords, 'LENGTH'),
|
|
69
|
+
address: getKeyword(keywords, 'ADDR'),
|
|
70
|
+
subpool: getKeyword(keywords, 'SP'),
|
|
71
|
+
key: getKeyword(keywords, 'KEY'),
|
|
72
|
+
cond: getKeyword(keywords, 'COND'),
|
|
73
|
+
keywords: Object.fromEntries(keywords)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseGetmain(st, ops, opts) {
|
|
78
|
+
const { positional, keywords } = ops;
|
|
79
|
+
const mode = (positional[0] ?? '').trim().toUpperCase() || null;
|
|
80
|
+
if (!mode && !keywords.has('MF')) {
|
|
81
|
+
throw new HlasmSyntax('GETMAIN requires a request type');
|
|
82
|
+
}
|
|
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)) {
|
|
87
|
+
throw new HlasmSyntax(`GETMAIN request type must be one of ${[...GETMAIN_MODES].join(', ')}, got ${positional[0]}`);
|
|
88
|
+
}
|
|
89
|
+
if (positional.slice(1).some((p) => p.trim() !== '')) {
|
|
90
|
+
throw new HlasmSyntax(`GETMAIN does not accept extra positional operands: ${positional[1]}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const allowed = new Set(['LA', 'LV', 'A', 'SP', 'BNDRY', 'CONTBDY', 'STARTBDY', 'KEY', 'LOC', 'INADDR', 'CHECKZERO', 'RELATED', 'MF', 'BRANCH', 'OWNER']);
|
|
94
|
+
for (const key of keywords.keys()) {
|
|
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)) {
|
|
99
|
+
throw new HlasmSyntax(`GETMAIN does not accept keyword ${key}`);
|
|
100
|
+
}
|
|
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);
|
|
106
|
+
|
|
107
|
+
const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
|
|
108
|
+
const address = getKeyword(keywords, 'A');
|
|
109
|
+
const subpool = getKeyword(keywords, 'SP');
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
kind: 'GETMAIN',
|
|
113
|
+
...legacy,
|
|
114
|
+
mode,
|
|
115
|
+
length,
|
|
116
|
+
address,
|
|
117
|
+
subpool,
|
|
118
|
+
...extra,
|
|
119
|
+
keywords: Object.fromEntries(keywords)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseFreemain(st, ops, opts) {
|
|
124
|
+
const { positional, keywords } = ops;
|
|
125
|
+
const mode = (positional[0] ?? '').trim().toUpperCase() || null;
|
|
126
|
+
if (!mode && !keywords.has('MF')) {
|
|
127
|
+
throw new HlasmSyntax('FREEMAIN requires a request type');
|
|
128
|
+
}
|
|
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)) {
|
|
133
|
+
throw new HlasmSyntax(`FREEMAIN request type must be one of ${[...FREEMAIN_MODES].join(', ')}, got ${positional[0]}`);
|
|
134
|
+
}
|
|
135
|
+
if (positional.slice(1).some((p) => p.trim() !== '')) {
|
|
136
|
+
throw new HlasmSyntax(`FREEMAIN does not accept extra positional operands: ${positional[1]}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const allowed = new Set(['LA', 'LV', 'A', 'SP', 'KEY', 'RELATED', 'MF', 'BRANCH']);
|
|
140
|
+
for (const key of keywords.keys()) {
|
|
141
|
+
if (!allowed.has(key)) {
|
|
142
|
+
throw new HlasmSyntax(`FREEMAIN does not accept keyword ${key}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const extra = remoteAndBranch('FREEMAIN', mode, keywords, FREEMAIN_LIST_MODES, FREEMAIN_GLOBAL_MODES);
|
|
146
|
+
|
|
147
|
+
const length = getKeyword(keywords, 'LV') ?? getKeyword(keywords, 'LA');
|
|
148
|
+
const address = getKeyword(keywords, 'A');
|
|
149
|
+
const subpool = getKeyword(keywords, 'SP');
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
kind: 'FREEMAIN',
|
|
153
|
+
...legacy,
|
|
154
|
+
mode,
|
|
155
|
+
length,
|
|
156
|
+
address,
|
|
157
|
+
subpool,
|
|
158
|
+
...extra,
|
|
159
|
+
keywords: Object.fromEntries(keywords)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const parsers = {
|
|
164
|
+
STORAGE: parseStorage,
|
|
165
|
+
GETMAIN: parseGetmain,
|
|
166
|
+
FREEMAIN: parseFreemain
|
|
167
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Parsers for the HLASM Toolkit structured programming macros (IF, DO, SELECT, etc.).
|
|
3
|
+
import { HlasmSyntax, NAME, splitOperands } from '../operands.mjs';
|
|
4
|
+
|
|
5
|
+
const CC_MNEMONICS = new Set(['H', 'L', 'E', 'Z', 'O', 'M', 'P', 'NH', 'NL', 'NE', 'NZ', 'NO', 'NM', 'NP', 'EQ', 'GT', 'LT', 'GE', 'LE',
|
|
6
|
+
'HE', 'LE', 'NHE', 'NLE']);
|
|
7
|
+
const CONNECTORS = new Set(['AND', 'OR', 'ANDIF', 'ORIF']);
|
|
8
|
+
|
|
9
|
+
// (Z) or (8): a condition-code mnemonic or a mask; (CLI,0(R1),EQ,C'A') or (LTR,R15,R15,NZ): an
|
|
10
|
+
// instruction and its operands with the condition among them.
|
|
11
|
+
function parseCondition(text) {
|
|
12
|
+
const t = text.trim();
|
|
13
|
+
if (!t.startsWith('(') || !t.endsWith(')')) {
|
|
14
|
+
if (CC_MNEMONICS.has(t.toUpperCase())) return t;
|
|
15
|
+
throw new HlasmSyntax(`unrecognised condition: ${t}`);
|
|
16
|
+
}
|
|
17
|
+
const parts = splitOperands(t.slice(1, -1)).map((x) => x.trim());
|
|
18
|
+
if (parts.length === 1) {
|
|
19
|
+
if (CC_MNEMONICS.has(parts[0].toUpperCase()) || /^(?:CC=)?(?:[1-9]|1[0-4])$/i.test(parts[0])) return t;
|
|
20
|
+
throw new HlasmSyntax(`unrecognised condition: ${t}`);
|
|
21
|
+
}
|
|
22
|
+
if (!NAME.test(parts[0])) throw new HlasmSyntax(`condition ${t} does not start with an instruction`);
|
|
23
|
+
if (!parts.slice(1).some((x) => CC_MNEMONICS.has(x.toUpperCase()))) throw new HlasmSyntax(`unrecognised condition: ${t} names no condition mnemonic`);
|
|
24
|
+
return t;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Conditions joined by AND, OR, ANDIF and ORIF, each operand of the macro one condition or joiner.
|
|
28
|
+
function parsePredicate(positional) {
|
|
29
|
+
const items = positional.map((x) => x.trim()).filter(Boolean);
|
|
30
|
+
if (!items.length) throw new HlasmSyntax('requires a predicate');
|
|
31
|
+
const conditions = [];
|
|
32
|
+
const joins = [];
|
|
33
|
+
items.forEach((x, i) => {
|
|
34
|
+
if (i % 2) {
|
|
35
|
+
if (!CONNECTORS.has(x.toUpperCase())) throw new HlasmSyntax(`expected AND, OR, ANDIF or ORIF, not ${x}`);
|
|
36
|
+
joins.push(x.toUpperCase());
|
|
37
|
+
} else conditions.push(parseCondition(x));
|
|
38
|
+
});
|
|
39
|
+
if (joins.length === conditions.length) throw new HlasmSyntax('a predicate cannot end with a joiner');
|
|
40
|
+
return { conditions, joins };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseIF(st, ops) {
|
|
44
|
+
const { conditions, joins } = parsePredicate(ops.positional);
|
|
45
|
+
return { kind: 'IF', conditions, joins, keywords: Object.fromEntries(ops.keywords) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseELSEIF(st, ops) {
|
|
49
|
+
const { conditions, joins } = parsePredicate(ops.positional);
|
|
50
|
+
return { kind: 'ELSEIF', conditions, joins, keywords: Object.fromEntries(ops.keywords) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseDO(st, ops) {
|
|
54
|
+
const pos = ops.positional[0] || '';
|
|
55
|
+
const kw = ops.keywords;
|
|
56
|
+
let form = 'SIMPLE';
|
|
57
|
+
if (pos) {
|
|
58
|
+
const upper = pos.toUpperCase();
|
|
59
|
+
if (upper === 'ONCE') form = 'ONCE';
|
|
60
|
+
else if (upper === 'INF') form = 'INF';
|
|
61
|
+
else if (upper === 'BXH' || upper === 'BXLE' || upper === 'BCT' || upper === 'BCTR') form = 'COUNT';
|
|
62
|
+
else if (pos.startsWith('(')) form = 'COUNT';
|
|
63
|
+
else if (upper === 'WHILE' || upper === 'UNTIL') {
|
|
64
|
+
if (kw.has(upper)) form = upper;
|
|
65
|
+
else throw new HlasmSyntax(`unknown DO positional operand: ${pos}`);
|
|
66
|
+
}
|
|
67
|
+
else throw new HlasmSyntax(`unknown DO positional operand: ${pos}`);
|
|
68
|
+
}
|
|
69
|
+
if (kw.has('FROM')) form = 'FROM';
|
|
70
|
+
else if (kw.has('WHILE')) form = 'WHILE';
|
|
71
|
+
else if (kw.has('UNTIL')) form = 'UNTIL';
|
|
72
|
+
return { kind: 'DO', form, keywords: Object.fromEntries(kw) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseDOEXIT(st, ops) {
|
|
76
|
+
const pos = ops.positional[0] || '';
|
|
77
|
+
if (pos) {
|
|
78
|
+
parseCondition(pos);
|
|
79
|
+
}
|
|
80
|
+
return { kind: 'DOEXIT', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseITERATE(st, ops) {
|
|
84
|
+
const pos = ops.positional[0] || '';
|
|
85
|
+
if (pos) {
|
|
86
|
+
if (!NAME.test(pos)) throw new HlasmSyntax(`ITERATE operand must be a label: ${pos}`);
|
|
87
|
+
}
|
|
88
|
+
return { kind: 'ITERATE', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseASMLEAVE(st, ops) {
|
|
92
|
+
const pos = ops.positional[0] || '';
|
|
93
|
+
if (pos && !NAME.test(pos)) throw new HlasmSyntax(`ASMLEAVE operand must be a label: ${pos}`);
|
|
94
|
+
return { kind: 'ASMLEAVE', operands: [pos], keywords: Object.fromEntries(ops.keywords) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// SELECT alone, or SELECT instruction,operand,condition: CLI,0(R6),EQ compares each WHEN's values.
|
|
98
|
+
function parseSELECT(st, ops) {
|
|
99
|
+
const parts = ops.positional.map((x) => x.trim()).filter(Boolean);
|
|
100
|
+
if (parts.length && (parts.length < 3 || !CC_MNEMONICS.has(parts[2].toUpperCase()))) throw new HlasmSyntax(`SELECT ${parts.join(',')} is not instruction,operand,condition`);
|
|
101
|
+
return { kind: 'SELECT', operands: [parts.join(',')].filter(Boolean), keywords: Object.fromEntries(ops.keywords) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// WHEN takes a condition, or after a comparing SELECT a parenthesised list of values: (X'20'), (1,5,13).
|
|
105
|
+
function parseWHEN(st, ops) {
|
|
106
|
+
const parts = ops.positional.map((x) => x.trim()).filter(Boolean);
|
|
107
|
+
if (!parts.length) throw new HlasmSyntax('WHEN needs a condition or values');
|
|
108
|
+
for (const p of parts) if (!(p.startsWith('(') && p.endsWith(')')) && !CONNECTORS.has(p.toUpperCase())) throw new HlasmSyntax(`WHEN operand ${p} is not in parentheses`);
|
|
109
|
+
return { kind: 'WHEN', operands: parts, keywords: Object.fromEntries(ops.keywords) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseNoOperand(st, ops) {
|
|
113
|
+
if (ops.positional.some((p) => p.trim() !== '')) throw new HlasmSyntax(`${st.operation} does not accept positional operands`);
|
|
114
|
+
return { kind: st.operation, keywords: Object.fromEntries(ops.keywords) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const parsers = {
|
|
118
|
+
IF: parseIF,
|
|
119
|
+
ELSEIF: parseELSEIF,
|
|
120
|
+
ELSE: parseNoOperand,
|
|
121
|
+
ENDIF: parseNoOperand,
|
|
122
|
+
DO: parseDO,
|
|
123
|
+
ENDDO: parseNoOperand,
|
|
124
|
+
DOEXIT: parseDOEXIT,
|
|
125
|
+
ITERATE: parseITERATE,
|
|
126
|
+
ASMLEAVE: parseASMLEAVE,
|
|
127
|
+
SELECT: parseSELECT,
|
|
128
|
+
WHEN: parseWHEN,
|
|
129
|
+
OTHRWISE: parseNoOperand,
|
|
130
|
+
ENDSEL: parseNoOperand
|
|
131
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Macro prototypes, model statements and open-code statements that hold variable symbols. A model
|
|
3
|
+
// statement is read as the statement it is: each variable symbol in its operands stands in for an
|
|
4
|
+
// ordinary symbol, or for the letter X inside a quoted string, and the result goes to the parser for
|
|
5
|
+
// its operation. An operation that is itself a variable symbol is decided at expansion and refused.
|
|
6
|
+
import { HlasmSyntax, NAME, splitOperands } from './operands.mjs';
|
|
7
|
+
import { parseHlasmStatement } from './read.mjs';
|
|
8
|
+
|
|
9
|
+
const VARIABLE = /^&[A-Z$#@_][A-Z0-9$#@_]*$/i;
|
|
10
|
+
// System variable symbols whose value has a fixed length, which is all a constant's length needs.
|
|
11
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=symbols-sysdate-system-variable-symbol
|
|
12
|
+
const FIXED_LENGTH = { '&SYSDATE': 8, '&SYSDATC': 8, '&SYSTIME': 5 };
|
|
13
|
+
|
|
14
|
+
// &NAME, with an optional subscript (&A(1), &A(&I+1)) and an optional period that joins it to what
|
|
15
|
+
// follows. && is one ampersand, not a variable symbol. Inside a quoted string a variable becomes as
|
|
16
|
+
// many X as its value has characters when that is fixed, and one X otherwise, counted as unsized.
|
|
17
|
+
// Outside one it becomes what `standIn` gives for the nth such variable.
|
|
18
|
+
function substitute(text, standIn = (n) => `VAR${n}`) {
|
|
19
|
+
let out = '';
|
|
20
|
+
let quoted = false;
|
|
21
|
+
let n = 0;
|
|
22
|
+
let unsized = 0;
|
|
23
|
+
for (let i = 0; i < text.length; i++) {
|
|
24
|
+
const c = text[i];
|
|
25
|
+
if (c === "'") { quoted = !quoted; out += c; continue; }
|
|
26
|
+
if (c !== '&') { out += c; continue; }
|
|
27
|
+
if (text[i + 1] === '&') { out += '&&'; i++; continue; }
|
|
28
|
+
const m = /^&[A-Z$#@_][A-Z0-9$#@_]*/i.exec(text.slice(i));
|
|
29
|
+
if (!m) { out += c; continue; }
|
|
30
|
+
i += m[0].length;
|
|
31
|
+
if (text[i] === '(') {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
for (; i < text.length; i++) { if (text[i] === '(') depth++; else if (text[i] === ')' && --depth === 0) { i++; break; } }
|
|
34
|
+
}
|
|
35
|
+
if (text[i] === '.') i++;
|
|
36
|
+
i--;
|
|
37
|
+
const fixed = FIXED_LENGTH[m[0].toUpperCase()];
|
|
38
|
+
if (quoted && !fixed) unsized++;
|
|
39
|
+
out += quoted ? 'X'.repeat(fixed || 1) : standIn(++n);
|
|
40
|
+
}
|
|
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;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function read(kind, st) {
|
|
59
|
+
if (/&[A-Z$#@_]/i.test(st.operation)) throw new HlasmSyntax(`the operation ${st.operation} is decided when the macro is expanded`);
|
|
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}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function prototype(st) {
|
|
80
|
+
if (st.name && !VARIABLE.test(st.name)) throw new HlasmSyntax(`a prototype's name field is a variable symbol or empty, not ${st.name}`);
|
|
81
|
+
if (!NAME.test(st.operation)) throw new HlasmSyntax(`${st.operation} is not a macro name`);
|
|
82
|
+
const positional = [];
|
|
83
|
+
const keywords = {};
|
|
84
|
+
for (const part of splitOperands(st.field || '')) {
|
|
85
|
+
const m = /^(&[A-Z$#@_][A-Z0-9$#@_]*)=([\s\S]*)$/i.exec(part);
|
|
86
|
+
if (m) keywords[m[1].toUpperCase()] = m[2];
|
|
87
|
+
else if (VARIABLE.test(part)) positional.push(part.toUpperCase());
|
|
88
|
+
else if (part !== '') throw new HlasmSyntax(`prototype operand ${part} is not a variable symbol`);
|
|
89
|
+
}
|
|
90
|
+
return { kind: 'PROTOTYPE', macro: st.operation, label: st.name || null, positional, keywords };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const parsers = {
|
|
94
|
+
PROTOTYPE: prototype,
|
|
95
|
+
MODEL: (st) => read('MODEL', st),
|
|
96
|
+
SUBSTITUTED: (st) => read('SUBSTITUTED', st),
|
|
97
|
+
};
|
|
@@ -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']);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// What every HLASM statement parser shares: the refusal it throws, and the split of an operand field
|
|
3
|
+
// into operands at the commas outside parentheses and quotes.
|
|
4
|
+
|
|
5
|
+
export class HlasmSyntax extends Error {
|
|
6
|
+
constructor(message) { super(message); this.name = 'HlasmSyntax'; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const NAME = /^[A-Z$#@_][A-Z0-9$#@_]{0,62}$/i;
|
|
10
|
+
export const ATTRIBUTES = 'LTSIKNDO';
|
|
11
|
+
|
|
12
|
+
// A quote after an attribute letter that stands alone, before a name, a variable symbol, * or a
|
|
13
|
+
// literal, is an attribute reference (L'FIELD), not the start of a string. The card reader
|
|
14
|
+
// (lib/bms.mjs) folds continuations by the same rule.
|
|
15
|
+
export const isAttributeQuote = (text, i) => i > 0 && ATTRIBUTES.includes(text[i - 1].toUpperCase())
|
|
16
|
+
&& !/[A-Z0-9$#@_&]/i.test(text[i - 2] || '') && /[A-Z$#@_&*=]/i.test(text[i + 1] || '');
|
|
17
|
+
|
|
18
|
+
export function splitOperands(field) {
|
|
19
|
+
const out = [];
|
|
20
|
+
let depth = 0, quoted = false, start = 0;
|
|
21
|
+
for (let i = 0; i < field.length; i++) {
|
|
22
|
+
const c = field[i];
|
|
23
|
+
if (quoted) {
|
|
24
|
+
if (c === "'") { if (field[i + 1] === "'") i++; else quoted = false; }
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (c === "'") { if (!isAttributeQuote(field, i)) quoted = true; }
|
|
28
|
+
else if (c === '(') depth++;
|
|
29
|
+
else if (c === ')') { if (--depth < 0) throw new HlasmSyntax(`a closing parenthesis has no opening one at column ${i + 1} of the operands`); }
|
|
30
|
+
else if (c === ',' && depth === 0) { out.push(field.slice(start, i)); start = i + 1; }
|
|
31
|
+
}
|
|
32
|
+
if (quoted) throw new HlasmSyntax('a quoted string in the operands is not closed');
|
|
33
|
+
if (depth) throw new HlasmSyntax('a parenthesis in the operands is not closed');
|
|
34
|
+
out.push(field.slice(start));
|
|
35
|
+
return field === '' ? [] : out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Positional operands until the first KEY=VALUE, as a macro call is written. A literal (=F'1') is a
|
|
39
|
+
// positional operand, not a keyword with an empty name.
|
|
40
|
+
export function macroOperands(field) {
|
|
41
|
+
const positional = [];
|
|
42
|
+
const keywords = new Map();
|
|
43
|
+
for (const part of splitOperands(field)) {
|
|
44
|
+
const m = /^([A-Z$#@_][A-Z0-9$#@_]*)=([\s\S]*)$/i.exec(part);
|
|
45
|
+
if (m) keywords.set(m[1].toUpperCase(), m[2]);
|
|
46
|
+
else positional.push(part);
|
|
47
|
+
}
|
|
48
|
+
return { positional, keywords };
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// HLASM's OPTABLE option: which operation code table the assembler reads machine instructions from.
|
|
3
|
+
// A mnemonic outside the chosen table is no instruction, so a macro of that name is called instead.
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const TABLE = new URL('../../rules/hlasm-optables.json', import.meta.url);
|
|
7
|
+
const T = existsSync(TABLE) ? JSON.parse(readFileSync(TABLE, 'utf8')) : { levels: [], synonyms: {}, machines: {}, mnemonics: {} };
|
|
8
|
+
const RANK = new Map(T.levels.map((level, i) => [level, i]));
|
|
9
|
+
|
|
10
|
+
// An OPTABLE or MACHINE suboption as written, to the table's level; null when it names none.
|
|
11
|
+
export function optableLevel(name, { machine = false } = {}) {
|
|
12
|
+
const n = String(name || '').trim().toUpperCase();
|
|
13
|
+
if (machine) return T.machines[n] ?? null;
|
|
14
|
+
if (n === 'UNI') return 'UNI';
|
|
15
|
+
return RANK.has(n) ? n : T.synonyms[n] ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// "UNI,DOS-370": UNI, a level, a level and later (X-), or a range (X-Y).
|
|
19
|
+
function covers(ranges, level) {
|
|
20
|
+
return ranges.split(',').some((part) => {
|
|
21
|
+
if (part === level) return true;
|
|
22
|
+
if (level === 'UNI') return false;
|
|
23
|
+
const m = /^([^-]+)-([^-]*)$/.exec(part);
|
|
24
|
+
if (!m || !RANK.has(m[1])) return false;
|
|
25
|
+
const at = RANK.get(level);
|
|
26
|
+
return at >= RANK.get(m[1]) && (m[2] === '' || at <= RANK.get(m[2]));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Whether the table at this level (UNI when none is given) holds the mnemonic; one the table does not
|
|
31
|
+
// list is taken as held.
|
|
32
|
+
const excluded = new Map();
|
|
33
|
+
export function inOptable(mnemonic, level) {
|
|
34
|
+
const at = level ?? 'UNI';
|
|
35
|
+
if (!excluded.has(at)) excluded.set(at, new Set(Object.entries(T.mnemonics).filter(([, ranges]) => !ranges.some((r) => covers(r, at))).map(([m]) => m)));
|
|
36
|
+
return !excluded.get(at).has(mnemonic);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// OPTABLE or MACHINE in an option string (PARM, *PROCESS), to a level; OVERRIDE(...) says it is forced.
|
|
40
|
+
export function optableIn(options) {
|
|
41
|
+
const text = String(options || '').toUpperCase();
|
|
42
|
+
const override = /OVERRIDE\(([^)]*(?:\([^)]*\)[^)]*)*)\)/.exec(text)?.[1] ?? '';
|
|
43
|
+
const read = (s) => {
|
|
44
|
+
const o = /\bOPTABLE\(\s*([A-Z0-9]+)/.exec(s);
|
|
45
|
+
if (o) return optableLevel(o[1]);
|
|
46
|
+
const m = /\bMACHINE\(\s*([A-Z0-9-]+)/.exec(s);
|
|
47
|
+
return m ? optableLevel(m[1], { machine: true }) : null;
|
|
48
|
+
};
|
|
49
|
+
const forced = read(override);
|
|
50
|
+
return forced ? { level: forced, override: true } : read(text) ? { level: read(text), override: false } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The *PROCESS statements that open a source file name its options.
|
|
54
|
+
export function processOptions(text) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const line of String(text).replace(/\r\n?/g, '\n').split('\n')) {
|
|
57
|
+
if (!/^\*PROCESS\s/i.test(line)) break;
|
|
58
|
+
out.push(line.slice(8, 71).trim());
|
|
59
|
+
}
|
|
60
|
+
return out.join(',');
|
|
61
|
+
}
|