diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 0bb5ce166..1e734695c 100644 --- a/docs/language_status/cobolrt_api.json +++ b/docs/language_status/cobolrt_api.json @@ -1,7 +1,7 @@ { "schema": "cobolrt-api/1", "about": ["The cobolrt public API (#4718). Written by tests/tools/cobolrt_api.py generate: edit only a `visibility`, to public or internal; docs/cobolrt_api_contract.md says what they promise.", "`evidence` records who called the item when it was last refreshed, and is never checked: det-port, model-port (committed ports), frozen-overlay (a frozen runtime copy's call to a live class), generated (translator output), emitter (a translator string), emitter-dynamic (a name a computed emission may take), runtime, harness; empty: no caller was found."], - "summary": {"types": 128, "types_public": 105, "members": 1198, "members_public": 809, "members_internal": 389}, + "summary": {"types": 128, "types_public": 105, "members": 1198, "members_public": 807, "members_internal": 391}, "types": [ {"id": "batch.CobolAbend", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/cobol_to_java_batch_forge.py", "variants": ["forge-batch-jobs", "forge-batch-files"], "supers": ["RuntimeException"], "visibility": "public", "evidence": ["det-port", "emitter", "model-port", "signature", "harness"], "members": [ {"id": "batch.CobolAbend.(String,String)", "kind": "constructor", "visibility": "internal", "evidence": []}, @@ -1010,8 +1010,8 @@ {"id": "cobolrt.JsonGenerate.array(String,Node...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["emitter-dynamic", "harness"]}, {"id": "cobolrt.JsonGenerate.generate(Field,Node,Charset)", "kind": "method", "static": true, "returns": "int", "visibility": "public", "evidence": ["emitter", "harness"]}, {"id": "cobolrt.JsonGenerate.group(String,Node...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["emitter-dynamic", "harness"]}, - {"id": "cobolrt.JsonGenerate.number(String,Field)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["harness"]}, - {"id": "cobolrt.JsonGenerate.text(String,Field)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["harness"]}, + {"id": "cobolrt.JsonGenerate.number(String,Field,String...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "internal", "evidence": ["harness"]}, + {"id": "cobolrt.JsonGenerate.text(String,Field,String...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "internal", "evidence": ["harness"]}, {"id": "cobolrt.JsonGenerate.tryGenerate(Field,Node,Charset)", "kind": "method", "static": true, "returns": "int", "visibility": "public", "evidence": ["emitter", "harness", "runtime"]} ]}, {"id": "cobolrt.JsonGenerate.Node", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java", "variants": ["det-batch", "det-standalone", "sort-job"], "supers": [], "visibility": "public", "evidence": ["signature", "harness", "runtime"], "members": [ diff --git a/docs/language_status/cobolrt_rules.json b/docs/language_status/cobolrt_rules.json index abce0d6a7..1cde927d6 100644 --- a/docs/language_status/cobolrt_rules.json +++ b/docs/language_status/cobolrt_rules.json @@ -118,6 +118,8 @@ "array", "read", "invalid", + "skip", + "allBytes", "text", "number", "quote", @@ -601,7 +603,7 @@ { "id": "RT-CBL-JSON-GENERATE", "family": "CBL", - "title": "JSON GENERATE from a group of DISPLAY, COMP / COMP-3 / COMP-5 items and fixed tables (an array per table): names as written, members in storage order, trailing blanks trimmed, numbers as edited, bytes written to the receiver and counted; a receiver too small is the exception condition under ON EXCEPTION (else a Hole), non-digit numeric data is a Hole (the oracle has no JSON statement)", + "title": "JSON GENERATE from a group of DISPLAY, COMP / COMP-3 / COMP-5 items and fixed tables (an array per table): names as written, members in storage order, trailing blanks trimmed, numbers as edited, bytes written to the receiver and counted; a receiver too small is the exception condition under ON EXCEPTION (else a Hole), SUPPRESS ... WHEN / EVERY leaves out an item whose value is ZERO / SPACE / LOW-VALUE / HIGH-VALUE, non-digit numeric data is a Hole (the oracle has no JSON statement)", "methods": [ "generate", "tryGenerate" diff --git a/docs/language_status/register/X76.md b/docs/language_status/register/X76.md index 8aff663e2..bdb50801f 100644 --- a/docs/language_status/register/X76.md +++ b/docs/language_status/register/X76.md @@ -4,11 +4,12 @@ family: "X" status: "ASSUMED" title: "JSON GENERATE from a group of DISPLAY, COMP / COMP-3 / COMP-5 items and fixed tables is translated from IBM's text alone — ASSUMED, oracle-unbacked (#7524)" area: "COBOL" -summary: "JSON GENERATE identifier-1 FROM group [COUNT IN] [NAME OF ... IS] [SUPPRESS item] over DISPLAY alphanumeric, fixed-point numeric (DISPLAY, COMP, COMP-3, COMP-5) items and fixed OCCURS tables is generated as a runtime call that writes IBM's JSON text (names as written, group wrapper, trailing blanks trimmed, numbers as edited). GnuCOBOL has no JSON statement, so nothing backs it: no L2+ claim. JSON-CODE after an exception, SUPPRESS WHEN / EVERY, CONVERTING, NAME OMITTED, OCCURS DEPENDING ON, a table as the source itself, float items, an elementary source and any character beyond ASCII stay holes or runtime Holes" +summary: "JSON GENERATE identifier-1 FROM group [COUNT IN] [NAME OF ... IS] [SUPPRESS item [WHEN ...] | EVERY [NUMERIC | NONNUMERIC] WHEN ...] over DISPLAY alphanumeric, fixed-point numeric (DISPLAY, COMP, COMP-3, COMP-5) items and fixed OCCURS tables is generated as a runtime call that writes IBM's JSON text (names as written, group wrapper, trailing blanks trimmed, numbers as edited). GnuCOBOL has no JSON statement, so nothing backs it: no L2+ claim. JSON-CODE after an exception, CONVERTING, NAME OMITTED, OCCURS DEPENDING ON, a table as the source itself, float items, an elementary source and any character beyond ASCII stay holes or runtime Holes" reached: "no (no burned program uses JSON; fixture tests/cobol_mainframe/test_det_json_generate.py)" --- -- **What IBM says** (Enterprise COBOL for z/OS 6.3 Language Reference, JSON GENERATE: https://www.ibm.com/docs/SS6SG3_6.3.0/lr/ref/rlpsjsog.html and its sub-pages). Data names are written as they are spelled in the program (case and hyphens kept); the source group's own name wraps its members as `{"GROUP":{...}}`; trailing spaces of an alphanumeric item are trimmed, an all-space item becomes one space; `"` `\` and control characters are escaped; a number is written as edited (a leading `-` for a negative value, leading zeros dropped to one integer digit, the decimals kept, no sign for a positive value); COUNT IN receives the bytes generated; FILLER / unnamed items, REDEFINES items and groups whose members are all ignored are left out; names must be unique; SUPPRESS names an item (a group takes its subtree); NAME OF renames an item; a receiver too small raises the exception condition (JSON-CODE nonzero, receiver undefined; ON EXCEPTION runs, else NOT ON EXCEPTION). +- **What IBM says** (Enterprise COBOL for z/OS 6.3 Language Reference, JSON GENERATE: https://www.ibm.com/docs/SS6SG3_6.3.0/lr/ref/rlpsjsog.html and its sub-pages). Data names are written as they are spelled in the program (case and hyphens kept); the source group's own name wraps its members as `{"GROUP":{...}}`; trailing spaces of an alphanumeric item are trimmed, an all-space item becomes one space; `"` `\` and control characters are escaped; a number is written as edited (a leading `-` for a negative value, leading zeros dropped to one integer digit, the decimals kept, no sign for a positive value); COUNT IN receives the bytes generated; FILLER / unnamed items, REDEFINES items and groups whose members are all ignored are left out; names must be unique; SUPPRESS names an item (a group takes its subtree); SUPPRESS item WHEN names an elementary item and leaves it out when its value is one of the listed figuratives (ZERO is a numeric comparison for a number, SPACE / LOW-VALUE / HIGH-VALUE select USAGE DISPLAY items, of the numeric ones only integers), SUPPRESS EVERY [NUMERIC | NONNUMERIC] WHEN does so for every item of that class, and a group left with no member goes too while the source group itself is always written (`{"A":{}}`); NAME OF renames an item; a receiver too small raises the exception condition (JSON-CODE nonzero, receiver undefined; ON EXCEPTION runs, else NOT ON EXCEPTION). - **Modelled.** `det/stmt.py` `_json_generate` parses the statement, `det/gen.py` `json_generate` builds the member tree and refuses the shapes below by name, `cobolrt/JsonGenerate.java` writes the text as US-ASCII bytes into the receiver (the bytes past the text are untouched). A JSON statement it cannot take keeps the `JSON GENERATE (...): not modelled` hole text, with the reason for the shape appended. - **What we assume.** (1) The elementary-source wrapper, the JSON-CODE values and a negative zero are not stated precisely enough: each is refused, not guessed. (2) Output is 7-bit ASCII only (UTF-8 and the EBCDIC code page agree there); any other character is a runtime `Hole`. (3) Non-digit data in a numeric DISPLAY item is **undefined by IBM** (register C11, family U): a runtime `Hole`. (4) A receiver shorter than the text is IBM's exception condition. With ON EXCEPTION / NOT ON EXCEPTION the port takes the exception branch and leaves the receiver and COUNT IN as they were (IBM leaves them undefined, so a program reading them there reads undefined data); without either phrase IBM sets JSON-CODE, which is not modelled: a runtime `Hole`. (5) A fixed table is an array under the item's name: an elementary item gives an array of values, a group an array of objects (IBM's table format); at most 256 items and elements are taken. A packed item with an invalid digit or sign nibble, or a binary item past its PICTURE digits, is undefined by IBM (C11): a runtime `Hole`. A COMP-5 item that overlaps another COMP-5 byte range is refused (its bytes are the oracle's). JSON PARSE keeps DISPLAY-only, table-free shapes. (6) IBM's DISPLAY has no ON EXCEPTION, so the phrases after a DISPLAY inside the first phrase belong to the JSON GENERATE. JSON PARSE and XML statements keep their phrases inside the hole. -- **Status:** ASSUMED, **oracle-unbacked**. GnuCOBOL has no JSON statement (the equivalence harness cannot compare it), so no L2+ claim rests on this entry; a port using it is reported as assumed (IBM-documented). JSON PARSE (X77), JSON-CODE after an exception, SUPPRESS WHEN / EVERY and DEPENDING ON tables are later slices of #7524. +- **Status:** ASSUMED, **oracle-unbacked**. GnuCOBOL has no JSON statement (the equivalence harness cannot compare it), so no L2+ claim rests on this entry; a port using it is reported as assumed (IBM-documented). JSON PARSE (X77), JSON-CODE after an exception, CONVERTING, NAME OMITTED and DEPENDING ON tables are later slices of #7524. +- **SUPPRESS ... WHEN / EVERY (#7524).** The figuratives are read per the IBM page (ZERO / ZEROS / ZEROES, SPACE(S), LOW-VALUE(S), HIGH-VALUE(S), joined by OR). Assumed: ZERO on an alphanumeric item is an alphanumeric comparison with ZEROS (every character `0`); SPACE / LOW-VALUE / HIGH-VALUE compare the item's bytes (SPACE is the code page's blank); a number equal to zero is suppressed by ZERO, so invalid data in it is a runtime `Hole` (C11) and a negative zero stays one. A named WHEN on a group, or on a USAGE the figurative does not select, is refused by name; the generic phrase silently skips the items it does not select. An item inside an OCCURS table is refused by name (the LR does not say whether one element or the table goes). JSON PARSE keeps refusing WHEN / EVERY. - **JSON-CODE (#7524).** A program that names JSON-CODE gets a hidden S9(4) item, set to 0 after a JSON GENERATE that raised no exception (the LR states this). The code IBM sets for an exception is in the Programming Guide's exception table, which the model does not state: a program that names JSON-CODE and has a JSON GENERATE with ON / NOT ON EXCEPTION, or any JSON PARSE, is refused by name. A program's own JSON-CODE item is kept. diff --git a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java index 30f13bd3a..bfa36e662 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java @@ -36,6 +36,11 @@ void render(StringBuilder out, Charset cs) { /** The value alone: what an element of a table is. */ abstract void value(StringBuilder out, Charset cs); + + /** True when SUPPRESS ... WHEN leaves this member out (its value is one of the WHEN figuratives, or, for a group, all its members are). */ + boolean skip(Charset cs) { + return false; + } } /** A table (OCCURS): `"name":[element,...]`, each element a value without a name (an object for a group). */ @@ -49,18 +54,20 @@ public static Node group(String name, Node... members) { } /** An alphanumeric / alphabetic elementary item: `"name":"text"`, trailing spaces trimmed, one space if all spaces. */ - public static Node text(String name, Field f) { - return new Text(name, f); + public static Node text(String name, Field f, String... when) { + return new Text(name, f, when); } /** A fixed-point DISPLAY number: `"name":-12.340`, leading zeros dropped, a plus sign never written. */ - public static Node number(String name, Field f) { - return new Number(name, f); + public static Node number(String name, Field f, String... when) { + return new Number(name, f, when); } /** * Writes `{root}` to `into` (the root's name is the source group's, or its NAME literal) and returns the - * number of bytes generated (COUNT IN). + * number of bytes generated (COUNT IN). `when` on an item (ZERO, SPACE, LOW-VALUE, HIGH-VALUE) is SUPPRESS ... WHEN: + * the item is left out when its value matches, and a group whose members are all left out goes with them; the + * root group is always written (IBM: `{"A":{}}`). */ // [rule RT-CBL-JSON-GENERATE] public static int generate(Field into, Node root, Charset cs) { @@ -129,14 +136,46 @@ private static final class Group extends Node { @Override void value(StringBuilder out, Charset cs) { out.append('{'); - for (int i = 0; i < members.size(); i++) { - if (i > 0) { + boolean first = true; + for (Node m : members) { + if (m.skip(cs)) { + continue; + } + if (!first) { out.append(','); } - members.get(i).render(out, cs); + first = false; + m.render(out, cs); } out.append('}'); } + + @Override + boolean skip(Charset cs) { + for (Node m : members) { + if (!m.skip(cs)) { + return false; + } + } + return true; + } + } + + /** + * Whether `raw` is entirely the figurative `w` (SPACE, LOW-VALUE, HIGH-VALUE; the byte of SPACE is the code page's). + */ + private static boolean allBytes(byte[] raw, String w, Charset cs) { + byte b = switch (w) { + case "SPACE" -> " ".getBytes(cs)[0]; + case "LOW-VALUE" -> (byte) 0x00; + default -> (byte) 0xFF; + }; + for (byte x : raw) { + if (x != b) { + return false; + } + } + return true; } private static final class Array extends Node { @@ -162,10 +201,23 @@ void value(StringBuilder out, Charset cs) { private static final class Text extends Node { final Field f; + final String[] when; - Text(String name, Field f) { + Text(String name, Field f, String[] when) { super(name); this.f = f; + this.when = when; + } + + /** ZERO on text is an alphanumeric comparison with ZEROS: every character a `0`. */ + @Override + boolean skip(Charset cs) { + for (String w : when) { + if (w.equals("ZERO") ? Cobol.text(f, cs).chars().allMatch(c -> c == '0') : allBytes(f.raw(), w, cs)) { + return true; + } + } + return false; } @Override @@ -181,10 +233,28 @@ void value(StringBuilder out, Charset cs) { private static final class Number extends Node { final Field f; + final String[] when; - Number(String name, Field f) { + Number(String name, Field f, String[] when) { super(name); this.f = f; + this.when = when; + } + + /** ZERO is a numeric comparison (invalid data and negative zero are the Holes of {@link #read}); the others + * compare the bytes of a DISPLAY integer. */ + @Override + boolean skip(Charset cs) { + for (String w : when) { + if (w.equals("ZERO")) { + if (read(cs).signum() == 0) { + return true; + } + } else if (allBytes(f.raw(), w, cs)) { + return true; + } + } + return false; } /** The value, or a Hole where IBM documents no result (invalid data) or the model is silent (negative zero). */ diff --git a/gitgalaxy/tools/cobol_to_java/det/gen.py b/gitgalaxy/tools/cobol_to_java/det/gen.py index cfdf1922d..27ce768c6 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3250,6 +3250,15 @@ def json_tree(self, d: dict, cls: str, what: str, src_ref: E.Ref) -> str: is refused by name.""" names = {id(self.resolve(r)): lit for r, lit in d["names"]} suppressed = {id(self.resolve(r)) for r in d["suppress"]} + # #7524: SUPPRESS item WHEN figurative ... (an elementary item) and SUPPRESS EVERY [NUMERIC | NONNUMERIC] WHEN ... + named_when: dict[int, list[str]] = {} + for r, vals in d["when"]: + w = self.resolve(r) + if w.category == "GROUP": + raise Untranslatable(f"{what}: SUPPRESS {w.name} WHEN names a group (IBM needs an elementary item)") + if any(v != "ZERO" and not self.json_when_fits(w, v) for v in vals): + raise Untranslatable(f"{what}: SUPPRESS {w.name} WHEN {' OR '.join(vals)} does not fit its usage") + named_when.setdefault(id(w), []).extend(vals) src = self.resolve(src_ref) if src.category != "GROUP" or src_ref.subscripts or src_ref.refmod is not None: raise Untranslatable(f"{what}: {src.name} is not a plain group (an elementary item is not modelled)") @@ -3289,7 +3298,14 @@ def elem(it: L.Item, name: str, idx: tuple[int, ...], top: bool = False) -> str leaves[0] += 1 if leaves[0] > JSON_TABLE_LEAVES: raise Untranslatable(f"{what}: more than {JSON_TABLE_LEAVES} items and table elements (not modelled)") - return f"__PACKAGE__.cobolrt.{cls}.{'number' if numeric else 'text'}({jstr(name)}, {f})" + vals = list(named_when.get(id(it), [])) + for kind, gv in d["generic"]: + if kind is None or (kind == "NUMERIC") == numeric: + vals += [v for v in gv if v == "ZERO" or self.json_when_fits(it, v)] + if vals and idx: + raise Untranslatable(f"{what}: SUPPRESS ... WHEN on {it.name}, an item in a table (not modelled)") + wh = "".join(f", {jstr(v)}" for v in dict.fromkeys(vals)) + return f"__PACKAGE__.cobolrt.{cls}.{'number' if numeric else 'text'}({jstr(name)}, {f}{wh})" def node(it: L.Item, top: bool, idx: tuple[int, ...]) -> str | None: """The Java node of `it`, or None when IBM leaves it out (FILLER, REDEFINES, SUPPRESSed, an empty group).""" @@ -3326,6 +3342,12 @@ def node(it: L.Item, top: bool, idx: tuple[int, ...]) -> str | None: return node(src, True, ()) or "" + @staticmethod + def json_when_fits(it: L.Item, value: str) -> bool: + """IBM (JSON GENERATE, SUPPRESS ... WHEN): SPACE, LOW-VALUE and HIGH-VALUE select USAGE DISPLAY items, and of the + zoned decimal ones only integers; ZERO selects every item.""" + return value == "ZERO" or (it.usage == "DISPLAY" and (it.category != "NUMERIC" or it.scale == 0)) + def json_generate(self, s: S.Stmt, ind: str) -> list[str]: """#7524: JSON GENERATE from a group of DISPLAY alphanumeric / numeric items (Enterprise COBOL 6.3 LR, X76).""" d = s.data diff --git a/gitgalaxy/tools/cobol_to_java/det/stmt.py b/gitgalaxy/tools/cobol_to_java/det/stmt.py index 62a866e4e..9d87d3dbe 100644 --- a/gitgalaxy/tools/cobol_to_java/det/stmt.py +++ b/gitgalaxy/tools/cobol_to_java/det/stmt.py @@ -1475,13 +1475,51 @@ def _json_name_phrase(p: E.Parser, names: list) -> None: raise E.ExprError("NAME with no item") -def _json_suppress_phrase(p: E.Parser, suppress: list) -> None: - """SUPPRESS item ...: appends the items; SUPPRESS ... WHEN / EVERY raises.""" +_JSON_WHEN = { + "ZERO": "ZERO", "ZEROS": "ZERO", "ZEROES": "ZERO", "SPACE": "SPACE", "SPACES": "SPACE", + "LOW-VALUE": "LOW-VALUE", "LOW-VALUES": "LOW-VALUE", "HIGH-VALUE": "HIGH-VALUE", "HIGH-VALUES": "HIGH-VALUE", +} # fmt: skip + + +def _json_when(p: E.Parser) -> list[str]: + """WHEN {ZERO | SPACE | LOW-VALUE | HIGH-VALUE} [OR ...] (IBM's when-phrase): the figuratives, spelled one way each.""" + p.take() + vals: list[str] = [] + while not p.done() and p.up() in _JSON_WHEN: + vals.append(_JSON_WHEN[p.take().upper()]) + if p.up() == "OR": + p.take() + if p.done() or p.up() not in _JSON_WHEN: + raise E.ExprError("WHEN ... OR without a figurative") + if not vals: + raise E.ExprError("WHEN without ZERO / SPACE / LOW-VALUE / HIGH-VALUE") + return vals + + +def _json_suppress_phrase(p: E.Parser, suppress: list, when: list | None = None, generic: list | None = None) -> None: + """SUPPRESS {item [WHEN figurative [OR ...]]} ... | EVERY [NUMERIC | NONNUMERIC] WHEN figurative [OR ...]: appends + the unconditional items to `suppress`, (item, [figurative]) to `when` and (class or None, [figurative]) to + `generic`. `when` None (JSON PARSE) raises on a WHEN / EVERY.""" p.take() while not p.done() and p.up() not in _JSON_PHRASES: - if p.up() in ("WHEN", "EVERY", "NUMERIC", "NONNUMERIC"): + if (when is None or generic is None) and p.up() in ("WHEN", "EVERY", "NUMERIC", "NONNUMERIC"): + raise E.ExprError("SUPPRESS ... WHEN / EVERY") + if p.up() == "EVERY": + if generic is None: # unreachable: JSON PARSE raised above + raise E.ExprError("SUPPRESS ... WHEN / EVERY") + p.take() + kind = p.take().upper() if p.up() in ("NUMERIC", "NONNUMERIC") else None + if p.up() != "WHEN": + raise E.ExprError("SUPPRESS EVERY without WHEN") + generic.append((kind, _json_when(p))) + elif p.up() in ("WHEN", "NUMERIC", "NONNUMERIC"): raise E.ExprError("SUPPRESS ... WHEN / EVERY") - suppress.append(p.ref()) + else: + item = p.ref() + if when is not None and p.up() == "WHEN": + when.append((item, _json_when(p))) + else: + suppress.append(item) if p.accept("WHEN"): raise E.ExprError("SUPPRESS ... WHEN / EVERY") @@ -1489,8 +1527,9 @@ def _json_suppress_phrase(p: E.Parser, suppress: list) -> None: def _json_generate(text: str, line: int) -> Stmt: """#7524: JSON GENERATE identifier-1 FROM identifier-2 [COUNT [IN] identifier-3] [NAME [OF] identifier-4 [IS] literal-1 ...] [SUPPRESS identifier-5 ...] [END-JSON] (Enterprise COBOL 6.3 Language Reference, JSON GENERATE). - data: into, src, count, names [(item, literal text)], suppress [item]. Everything else IBM defines on the statement - (ON / NOT ON EXCEPTION, CONVERTING, SUPPRESS ... WHEN / EVERY, NAME OMITTED, a literal that is not alphanumeric) + data: into, src, count, names [(item, literal text)], suppress [item], when [(item, [figurative])] (SUPPRESS item + WHEN), generic [(NUMERIC | NONNUMERIC | None, [figurative])] (SUPPRESS EVERY ... WHEN). Everything else IBM defines + on the statement (CONVERTING, NAME OMITTED, a literal that is not alphanumeric) raises ExprError, and the caller holes the statement with that as the shape.""" p = E.Parser(E.tokenize(text)[2:]) into = p.ref() @@ -1500,6 +1539,8 @@ def _json_generate(text: str, line: int) -> Stmt: count = None names: list[tuple[E.Ref, str]] = [] suppress: list[E.Ref] = [] + when: list[tuple[E.Ref, list[str]]] = [] + generic: list[tuple[str | None, list[str]]] = [] while not p.done(): u = p.up() if u == "COUNT": @@ -1509,12 +1550,25 @@ def _json_generate(text: str, line: int) -> Stmt: elif u == "NAME": _json_name_phrase(p, names) elif u == "SUPPRESS": - _json_suppress_phrase(p, suppress) + _json_suppress_phrase(p, suppress, when, generic) elif u == "END-JSON": p.take() else: raise E.ExprError(f"{u} phrase") - return Stmt("JSONGEN", line, text, {"into": into, "src": src, "count": count, "names": names, "suppress": suppress}) + return Stmt( + "JSONGEN", + line, + text, + { + "into": into, + "src": src, + "count": count, + "names": names, + "suppress": suppress, + "when": when, + "generic": generic, + }, + ) def _json_parse(text: str, line: int) -> Stmt: @@ -1557,7 +1611,10 @@ def _json_parse(text: str, line: int) -> Stmt: else: raise E.ExprError(f"{u} phrase") return Stmt( - "JSONPARSE", line, text, {"src": src, "into": into, "detail": detail, "names": names, "suppress": suppress} + "JSONPARSE", + line, + text, + {"src": src, "into": into, "detail": detail, "names": names, "suppress": suppress, "when": [], "generic": []}, ) diff --git a/tests/cobol_mainframe/test_det_json_generate.py b/tests/cobol_mainframe/test_det_json_generate.py index aca132e6e..e11a2ddf7 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -67,7 +67,9 @@ def test_name_of_and_suppress_shape_the_members(tmp_path): @pytest.mark.parametrize( "stmt, why", [ - ("JSON GENERATE OUT FROM REC SUPPRESS AMT WHEN ZERO", "SUPPRESS ... WHEN / EVERY"), + ("JSON GENERATE OUT FROM REC SUPPRESS AMT WHEN ZERO OR", "WHEN ... OR without a figurative"), + ("JSON GENERATE OUT FROM REC SUPPRESS EVERY NUMERIC", "SUPPRESS EVERY without WHEN"), + ("JSON GENERATE OUT FROM REC SUPPRESS WHEN ZERO", "SUPPRESS ... WHEN / EVERY"), ( "JSON GENERATE OUT FROM REC CONVERTING ON EXCEPTION\n DISPLAY 'X'\n END-JSON", "CONVERTING phrase", @@ -126,11 +128,11 @@ def test_phrases_scope_to_their_own_statement(tmp_path): " END-JSON", " DISPLAY 'AFTER'", "END-IF", - "JSON GENERATE OUT FROM REC SUPPRESS AMT WHEN ZERO", + "JSON GENERATE OUT FROM REC NAME OF AMT IS OMITTED", " ON EXCEPTION MOVE 2 TO N", "END-JSON", ) - assert len(r.stats["holes"]) == 1 and "SUPPRESS ... WHEN / EVERY" in str(r.stats["holes"][0]) + assert len(r.stats["holes"]) == 1 and "NAME needs an alphanumeric literal" in str(r.stats["holes"][0]) def test_xml_and_parse_phrases_stay_in_their_hole(tmp_path): @@ -383,3 +385,136 @@ def test_a_program_s_own_json_code_item_is_kept(tmp_path): src.write_text(text + " JSON GENERATE OUT FROM REC\n DISPLAY JSON-CODE\n GOBACK.\n") r = P.translate(src, [], "public class ProgService { }", "t") assert "GG_JSON_CODE" not in r.java.upper().replace("-", "_") + + +def _gen_line(r) -> str: + return next(x for x in r.java.splitlines() if "JsonGenerate.generate" in x) + + +def test_suppress_when_names_the_figuratives_per_item(tmp_path): + """#7524: SUPPRESS item WHEN {ZERO | SPACE | LOW-VALUE | HIGH-VALUE} [OR ...] -- every spelling, one runtime word.""" + r = _translate( + tmp_path, + "JSON GENERATE OUT FROM REC SUPPRESS AMT WHEN ZEROES", + " CUST-NAME WHEN SPACES OR LOW-VALUES CITY WHEN HIGH-VALUES", + ) + assert r.stats["holes"] == [] + line = _gen_line(r) + assert 'number("AMT", ' in line and '"ZERO")' in line and '"HIGH-VALUE")' in line + assert '"SPACE", "LOW-VALUE")' in line + for word, norm in (("ZERO", "ZERO"), ("ZEROS", "ZERO"), ("SPACE", "SPACE"), ("LOW-VALUES", "LOW-VALUE")): + line = _gen_line(_translate(tmp_path, f"JSON GENERATE OUT FROM REC SUPPRESS CUST-NAME WHEN {word}")) + assert f'"{norm}")' in line + + +def test_generic_suppress_every_selects_by_class(tmp_path): + num = _gen_line(_translate(tmp_path, "JSON GENERATE OUT FROM REC SUPPRESS EVERY", "NUMERIC WHEN ZERO")) + assert 'number("AMT", ' in num and '"ZERO")' in num.split('number("AMT"')[1].split("),")[0] + ")" + assert 'text("CUST-NAME", ' in num and '"ZERO"' not in num.split('text("CUST-NAME"')[1].split("),")[0] + non = _gen_line(_translate(tmp_path, "JSON GENERATE OUT FROM REC SUPPRESS EVERY", "NONNUMERIC WHEN SPACES")) + assert non.count('"SPACE"') == 2 # CUST-NAME and CITY; AMT is a number with decimals + both = _gen_line(_translate(tmp_path, "JSON GENERATE OUT FROM REC SUPPRESS EVERY WHEN LOW-VALUE")) + assert both.count('"LOW-VALUE"') == 2 # the decimals of AMT keep it out of SPACE / LOW / HIGH + + +@pytest.mark.parametrize( + ("stmt", "why"), + [ + ("JSON GENERATE OUT FROM REC SUPPRESS ADDR WHEN SPACE", "names a group"), + ("JSON GENERATE OUT FROM REC SUPPRESS AMT WHEN SPACE", "does not fit its usage"), + ("JSON PARSE OUT INTO REC SUPPRESS AMT WHEN ZERO", "SUPPRESS ... WHEN / EVERY"), + ("JSON PARSE OUT INTO REC SUPPRESS EVERY NUMERIC WHEN ZERO", "SUPPRESS ... WHEN / EVERY"), + ], +) +def test_suppress_when_shapes_the_model_does_not_take_are_refused_by_name(tmp_path, stmt, why): + r = _translate(tmp_path, stmt) + assert len(r.stats["holes"]) == 1 and why in str(r.stats["holes"][0]) + + +def test_suppress_when_on_a_table_item_is_refused_by_name(tmp_path): + for stmt in ( + "JSON GENERATE OUT FROM REC SUPPRESS QTY WHEN ZERO", + "JSON GENERATE OUT FROM REC SUPPRESS EVERY NUMERIC WHEN ZERO", + "JSON GENERATE OUT FROM REC SUPPRESS CODES WHEN SPACE", + ): + r = _wide(tmp_path, stmt) + assert len(r.stats["holes"]) == 1 + assert "an item in a table" in str(r.stats["holes"][0]) or "OCCURS" in str(r.stats["holes"][0]) + + +def test_the_runtime_leaves_out_what_suppress_when_selects(tmp_path): + from gitgalaxy.tools.cobol_to_java.cobol_to_java_transaction_forge import CICS_SPEC_JAVA, CICS_TASK_JAVA + + src = tmp_path / "src" + files = {"t/cics/CicsTask.java": CICS_TASK_JAVA.replace("__PACKAGE__", "t").replace("__ZONE__", "UTC"), + "t/cics/CicsSpec.java": CICS_SPEC_JAVA.replace("__PACKAGE__", "t"), + **{f"t/{k}": v for k, v in P.runtime_files("t", batch=False).items()}} # fmt: skip + files["Main.java"] = """ +import java.nio.charset.Charset; +import t.cobolrt.Field; +import t.cobolrt.JsonGenerate; +import t.cobolrt.Storage; +public class Main { + static Charset CS = Charset.forName("ISO-8859-1"); + // 0: A X(4) 4: N 9(3) 7: P S9(3) COMP-3 9: B 9(2)V9 + static void show(String what, String a, String n, int[] packed, String... w) { + Storage s = new Storage(200); + byte[] ab = a.getBytes(CS), nb = n.getBytes(CS); + for (int i = 0; i < 4; i++) s.bytes[i] = ab[i]; + for (int i = 0; i < 3; i++) s.bytes[4 + i] = nb[i]; + for (int i = 0; i < 2; i++) s.bytes[7 + i] = (byte) packed[i]; + Field out = Field.alphanumeric(s, 100, 100, false); + String[] none = new String[0]; + JsonGenerate.Node ta = JsonGenerate.text("A", Field.alphanumeric(s, 0, 4, false), w.length > 0 ? w[0].split(",") : none); + JsonGenerate.Node tn = JsonGenerate.number("N", Field.zoned(s, 4, 3, 0, false, false, false), w.length > 1 ? w[1].split(",") : none); + JsonGenerate.Node tp = JsonGenerate.number("P", Field.packed(s, 7, 3, 0, true), w.length > 2 ? w[2].split(",") : none); + JsonGenerate.Node inner = JsonGenerate.group("I", tn, tp); + try { + int c = JsonGenerate.generate(out, JsonGenerate.group("G", ta, inner), CS); + System.out.println(what + " " + new String(s.bytes, 100, c, CS)); + } catch (RuntimeException e) { + System.out.println(what + " refused: " + e.getMessage()); + } + } + public static void main(String[] x) { + int[] pk = {0x01, 0x2C}; + int[] pz = {0x00, 0x0C}; + show("none", "ab ", "007", pk); + show("textzero", "0000", "007", pk, "ZERO"); + show("textnotzero", "0a00", "007", pk, "ZERO"); + show("space", " ", "007", pk, "SPACE"); + show("spacenot", " a ", "007", pk, "SPACE"); + show("low", "\u0000\u0000\u0000\u0000", "007", pk, "LOW-VALUE"); + show("high", "\u00ff\u00ff\u00ff\u00ff", "007", pk, "HIGH-VALUE"); + show("either", " ", "007", pk, "ZERO,SPACE"); + show("numzero", "ab ", "000", pk, "", "ZERO"); + show("packzero", "ab ", "007", pz, "", "", "ZERO"); + show("innergone", "ab ", "000", pz, "", "ZERO", "ZERO"); + show("allgone", " ", "000", pz, "SPACE", "ZERO", "ZERO"); + show("junkzero", "ab ", "0a0", pk, "", "ZERO"); + show("numspace", "ab ", " ", pk, "", "SPACE"); + } +} +""" + for rel, text in files.items(): + (src / rel).parent.mkdir(parents=True, exist_ok=True) + (src / rel).write_text(text, encoding="utf-8") + jdk = _jdk() + built = javac_run([str(jdk / "javac"), "-encoding", "UTF-8", "-nowarn", "-d", str(tmp_path / "classes"), + *map(str, src.rglob("*.java"))], capture_output=True, text=True, encoding="utf-8", check=False) # fmt: skip + assert built.returncode == 0, built.stderr + out = subprocess.run([str(jdk / "java"), "-Dfile.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-cp", str(tmp_path / "classes"), "Main"], # noqa: S603 + capture_output=True, text=True, encoding="utf-8", check=True).stdout.splitlines() # fmt: skip + got = dict(line.split(" ", 1) for line in out) + assert got["none"] == '{"G":{"A":"ab","I":{"N":7,"P":12}}}' + assert got["textzero"] == '{"G":{"I":{"N":7,"P":12}}}' + assert got["textnotzero"].startswith('{"G":{"A":"0a00"') + assert got["space"] == '{"G":{"I":{"N":7,"P":12}}}' + assert got["spacenot"].startswith('{"G":{"A":" a"') + assert got["low"] == got["high"] == got["either"] == got["space"] + assert got["numzero"] == '{"G":{"A":"ab","I":{"P":12}}}' + assert got["packzero"] == '{"G":{"A":"ab","I":{"N":7}}}' + assert got["innergone"] == '{"G":{"A":"ab"}}' # a group with nothing left goes too + assert got["allgone"] == '{"G":{}}' # the source group itself is always written + assert "non-digit data in numeric item N" in got["junkzero"] + assert got["numspace"] == '{"G":{"A":"ab","I":{"P":12}}}' diff --git a/tests/cobol_mainframe/test_det_source_4462.py b/tests/cobol_mainframe/test_det_source_4462.py index 16054a3fa..c3581155e 100644 --- a/tests/cobol_mainframe/test_det_source_4462.py +++ b/tests/cobol_mainframe/test_det_source_4462.py @@ -785,7 +785,7 @@ def test_a_literal_cut_at_column_72_is_refused_as_a_source_defect(): (("JSON PARSE A(1:B) INTO R SUPPRESS D WHEN SPACES",), "JSON PARSE"), (("JSON PARSE A INTO R WITH DETAIL NAME OF C IS 'c' SUPPRESS D WHEN SPACES ON EXCEPTION DISPLAY 'BAD' MOVE 1 TO B", " NOT ON EXCEPTION DISPLAY 'OK'", "END-JSON"), "JSON PARSE"), - (("JSON GENERATE A FROM R COUNT IN B SUPPRESS C WHEN SPACES",), "JSON GENERATE"), + (("JSON GENERATE A FROM R COUNT IN B WITH ENCODING 1208",), "JSON GENERATE"), (("XML GENERATE A FROM R COUNT IN B ON EXCEPTION DISPLAY 'X'", "END-XML"), "XML GENERATE"), (("XML PARSE A PROCESSING PROCEDURE P2",), "XML PARSE"), ],