Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/language_status/cics_command_spec.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Design: one declarative CICS command spec (#4270)

**Status: design accepted with the owner's decisions (2026-10-06, section 9); PR 1 (the package, its CLI and the
transitional equality tests) merged (#4589); PR 2 (the translator reads the spec; 45 full entries, 206 name-only) in
review.** All line numbers are against origin/main `b6c3e4ea8`.
transitional equality tests) merged (#4589); PR 2 (the translator reads the spec; 45 full entries, 206 name-only)
merged (#4591); PR 3 (the stub translator reads the spec) merged (#4593); PR 4 (the generated, committed runtime
tables) in review.** All line numbers are against origin/main `b6c3e4ea8`.

PR 4 as built: `python -m gitgalaxy.standards.cics regen` writes `gitgalaxy/tools/cobol_to_java/CicsSpec.java`
(package `<pkg>.cics`, emitted by the transaction forge beside `CicsTask`, since a forge-only project has no
`cobolrt`; `DetCics` imports it from there) and `tests/equivalence/cics/ggcics_spec.h` (`DFHRESP_<condition>`,
`condition_abcode`; `ggcics.c`'s short condition names are defined from it). `regen --check` is the `cics-spec`
ratchet. Only the RESP and abend tables are generated so far; the refusal constants and the stated facts are PR 5's
(`cics_crucible.CONDITION_ABCODE` too: it is the runner's table, not a runtime's).

Every #4270 slice teaches the det port one more EXEC CICS command by writing the same facts about that command
again in five to seven places, in three languages. The engine pulls CICS facts out of the same commands with its own
Expand Down
2 changes: 1 addition & 1 deletion docs/language_status/cics_spec_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Census use is `programs (burned / non-burned)`, counts only. A spec key is the d
| 1 | done (#4589) | gitgalaxy/standards/cics/ model + resp.py (DFHRESP, abend codes) + entries for the slice 1-4 commands, with today's text word for word | none |
| 2 | done (#4591) | full entries for the other 34 OPTIONS commands (PR 1 has the 9 of slices 1-4) + LOAD / RELEASE (45 in all) | translator |
| 3 | done (#4593) | equivalence_cics.py refusals / options / DFHRESP from the spec | stub translator |
| 4 | open | generated, committed runtime tables: CicsSpec.java (delegated to by DetCics condition / resp, respName, abcodeFor) and ggcics_spec.h | both runtimes |
| 4 | done (#4608) | generated, committed runtime tables: CicsSpec.java (delegated to by DetCics condition / resp, respName, abcodeFor) and ggcics_spec.h | both runtimes |
| 5 | open | Fact wiring in cics_crucible.py | runner, both runtimes |
| 6 | open | outcome conformance check (3.2), report-only for one week, then an undeclared (RESP, RESP2) fails the crucible cell | harness, CI |
| 7 | open | check-register in pr_gates.py | docs, skill |
Expand Down
139 changes: 103 additions & 36 deletions gitgalaxy/standards/cics/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

emit [--json] [KEY ...] the entries (all, or the commands named), as a summary or as JSON (never committed)
check the spec's own consistency (beyond what each dataclass checks when it is built)
regen --out DIR render the generated runtime tables (CicsSpec.java, ggcics_spec.h) into DIR
regen [--check | --out DIR] rewrite the committed generated runtime tables (gitgalaxy/tools/cobol_to_java/
CicsSpec.java, tests/equivalence/cics/ggcics_spec.h); --check: exit 1 when one is
stale (the cics-spec ratchet, pr_gates.py --ratchets); --out: render them into DIR

PR 1 commits no generated file: PR 4 commits them beside the runtimes, delegates the hand-written switches to
them and adds the drift test."""
Spec PR 4 committed them: DetCics.condition / resp, CicsTask.respName / abcodeFor and ggcics.c's condition enums /
condition_abcode delegate to them. Only tables are generated; every runtime's behaviour stays hand-written."""

from __future__ import annotations

Expand Down Expand Up @@ -74,88 +76,141 @@ def problems() -> list[str]:
return out


def render_java(package: str) -> str:
"""CicsSpec.java: the RESP / abend switches DetCics.condition / resp, CicsTask.respName / abcodeFor delegate to
(from PR 4)."""
from gitgalaxy.standards.cics import resp
# The committed generated files (spec PR 4), relative to the repository root. CicsSpec.java sits beside the
# transaction forge, which emits it next to CicsTask (package <pkg>.cics: a forge-only project has no cobolrt), and
# DetCics imports it from there; ggcics_spec.h sits beside ggcics.c, which includes it.
ROOT = Path(__file__).resolve().parents[3]
JAVA_FILE = Path("gitgalaxy/tools/cobol_to_java/CicsSpec.java")
C_FILE = Path("tests/equivalence/cics/ggcics_spec.h")
JAVA_PACKAGE = "__PACKAGE__.cics" # the forge fills __PACKAGE__ in, as it does in CicsTask

def cases(pairs: list[tuple[str, str]]) -> str:
return "\n".join(f" case {k} -> {v};" for k, v in pairs)

names = cases([(str(n), f'"{c}"') for n, c in sorted(resp.RESP_NAME.items())])
numbers = cases([(f'"{c}"', str(n)) for c, n in resp.DFHRESP.items()])
abends = cases([(f'"{c}"', f'"{a}"') for c, a in resp.CONDITION_ABEND.items()])
return f"""// {BANNER}
package {package};
# Each generated file is a list of sections, one per spec table. A later table (the stated facts of PR 5, the EIB
# facts) is one more renderer appended to JAVA_SECTIONS / C_SECTIONS: the file around it does not change.
def _java_resp() -> str:
from gitgalaxy.standards.cics import resp

/** The CICS command spec's tables (IBM CICS TS, RESP values; the AEIx / AEYx / AEZx abend codes). */
public final class CicsSpec {{
private CicsSpec() {{
names = "\n".join(f' case {n} -> "{c}";' for n, c in sorted(resp.RESP_NAME.items()))
numbers = "\n".join(f' case "{c}" -> {n};' for c, n in resp.DFHRESP.items())
return f""" /** The condition name of a RESP value (IBM CICS TS, RESP values: DFHRESP; never an alias), null for a number
* DFHRESP has no name for. */
public static String name(int resp) {{
return switch (resp) {{
{names}
default -> null;
}};
}}

/** The condition a RESP value names; "RESP" + the number for one DFHRESP has no name for. */
public static String condition(int resp) {{
return switch (resp) {{
{names}
default -> "RESP" + resp;
}};
String name = name(resp);
return name != null ? name : "RESP" + resp;
}}

/** The RESP value of a condition. */
/** The RESP value of a condition (an alias too: DSIDERR is FILENOTFOUND's number). */
public static int resp(String condition) {{
return switch (condition) {{
{numbers}
default -> throw new IllegalArgumentException("no RESP value known for condition " + condition);
}};
}}
"""

/** The abend code of an unhandled condition. */

def _java_abend() -> str:
from gitgalaxy.standards.cics import resp

abends = "\n".join(f' case "{c}" -> "{a}";' for c, a in resp.CONDITION_ABEND.items())
return f""" /** The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic). */
public static String abcodeFor(String condition) {{
return switch (condition) {{
{abends}
default -> throw new IllegalArgumentException("no abend code known for condition " + condition);
}};
}}
}}
"""


def render_c() -> str:
"""ggcics_spec.h: the DFHRESP enum and condition_abcode for the stub (from PR 4)."""
def _c_resp() -> str:
from gitgalaxy.standards.cics import resp

enum = ",\n".join(f" DFHRESP_{c} = {n}" for c, n in resp.DFHRESP.items())
cases = "\n".join(f' case DFHRESP_{c}: return "{a}";' for c, a in resp.CONDITION_ABEND.items())
return f"""/* {BANNER} */
#ifndef GGCICS_SPEC_H
#define GGCICS_SPEC_H

return f"""/* IBM CICS TS, RESP values (DFHRESP): the condition name -> its RESP number */
enum {{
{enum}
}};
"""


def _c_abend() -> str:
from gitgalaxy.standards.cics import resp

/* The abend code of an unhandled condition; "????" for one the spec has none for. */
cases = "\n".join(f' case DFHRESP_{c}: return "{a}";' for c, a in resp.CONDITION_ABEND.items())
return f"""/* The abend code of an unhandled condition (IBM's AEIx / AEYx / AEZx abend codes, the AEIA topic); "????" for
one the spec has none for. */
static const char *condition_abcode(int resp) {{
switch (resp) {{
{cases}
default: return "????";
}}
}}
"""


JAVA_SECTIONS = [_java_resp, _java_abend]
C_SECTIONS = [_c_resp, _c_abend]


def render_java(package: str = JAVA_PACKAGE) -> str:
"""CicsSpec.java: the tables DetCics.condition / resp and CicsTask.respName / abcodeFor delegate to."""
body = "\n".join(section() for section in JAVA_SECTIONS)
return f"""// {BANNER}
package {package};

/** The CICS command spec's tables (gitgalaxy/standards/cics): RESP values and the default abend codes. */
public final class CicsSpec {{
private CicsSpec() {{
}}

{body}}}
"""


def render_c() -> str:
"""ggcics_spec.h: the DFHRESP enum (DFHRESP_<condition>) and condition_abcode, which ggcics.c includes."""
body = "\n".join(section() for section in C_SECTIONS)
return f"""/* {BANNER} */
#ifndef GGCICS_SPEC_H
#define GGCICS_SPEC_H

{body}
#endif
"""


def generated() -> dict[Path, str]:
"""The committed generated files (relative to the repository root) and the text the spec renders for each."""
return {JAVA_FILE: render_java(), C_FILE: render_c()}


def stale(root: Path = ROOT) -> list[Path]:
"""The committed generated files that differ from what the spec renders: [] when none is stale."""
return [rel for rel, text in generated().items()
if not (root / rel).is_file() or (root / rel).read_text(encoding="utf-8") != text] # fmt: skip


def main(argv: Sequence[str] | None = None) -> int:
ap = argparse.ArgumentParser(prog="python -m gitgalaxy.standards.cics", description=__doc__.splitlines()[0])
sub = ap.add_subparsers(dest="cmd", required=True)
e = sub.add_parser("emit", help="print the entries")
e.add_argument("keys", nargs="*", help="command keys (default: all), e.g. 'GET CONTAINER'")
e.add_argument("--json", action="store_true", help="the full entries as JSON")
sub.add_parser("check", help="the spec's own consistency")
r = sub.add_parser("regen", help="render the generated runtime tables into a directory")
r.add_argument("--out", type=Path, required=True, help="where to write CicsSpec.java and ggcics_spec.h")
r.add_argument("--java-package", default="cobolrt.cics", help="the generated Java class's package")
r = sub.add_parser("regen", help="rewrite the committed generated runtime tables (or check / render them)")
how = r.add_mutually_exclusive_group()
how.add_argument("--check", action="store_true", help="exit 1, listing them, when a committed file is stale")
how.add_argument("--out", type=Path, help="render CicsSpec.java and ggcics_spec.h into this directory instead")
r.add_argument("--java-package", default=JAVA_PACKAGE, help="with --out: the generated Java class's package")
args = ap.parse_args(argv)

if args.cmd == "emit":
Expand All @@ -180,7 +235,19 @@ def main(argv: Sequence[str] | None = None) -> int:
n = len(_commands())
print(f"cics spec: {n} commands, {'ok' if not found else f'{len(found)} problem(s)'}")
return 1 if found else 0
if not re.fullmatch(r"[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*", args.java_package):
if args.check:
old = stale()
for path in old:
print(f"stale: {path}")
print(f"cics spec generated files: {'up to date' if not old else f'{len(old)} stale'}"
+ ("; run `python -m gitgalaxy.standards.cics regen` and commit them" if old else "")) # fmt: skip
return 1 if old else 0
if args.out is None:
for path, text in generated().items():
(ROOT / path).write_text(text, encoding="utf-8")
print(f"wrote {path}")
return 0
if not re.fullmatch(r"(__PACKAGE__|[a-z_][a-z0-9_]*)(\.[a-z_][a-z0-9_]*)*", args.java_package):
print(f"not a Java package name: {args.java_package}", file=sys.stderr)
return 2
args.out.mkdir(parents=True, exist_ok=True)
Expand Down
Loading
Loading