From 7c22a5c5ded12c6b68c7acd6900b47a51b2efd19 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 13:50:55 -0500 Subject: [PATCH] det: JSON GENERATE NAME OF the source IS OMITTED writes an anonymous object (Part of #7524) IBM's NAME phrase: OMITTED generates an anonymous JSON object whose top-level parent name is not written, and identifier-4 must be identifier-2. Translated as the source group's {members} alone (oracle-unbacked, register X76); OMITTED on any other item stays refused by name; JSON PARSE keeps refusing it. Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/cobolrt_api.json | 3 +- docs/language_status/cobolrt_rules.json | 1 + docs/language_status/register/X76.md | 6 +-- .../det/cobolrt/JsonGenerate.java | 33 +++++++++++++++-- gitgalaxy/tools/cobol_to_java/det/gen.py | 14 ++++++- gitgalaxy/tools/cobol_to_java/det/stmt.py | 15 +++++--- .../cobol_mainframe/test_det_json_generate.py | 37 ++++++++++++++++++- 7 files changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 6aa1f6574a..12b9faef4f 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": 1199, "members_public": 808, "members_internal": 391}, + "summary": {"types": 128, "types_public": 105, "members": 1201, "members_public": 810, "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": []}, @@ -1007,6 +1007,7 @@ {"id": "cobolrt.Hole.(String)", "kind": "constructor", "visibility": "public", "evidence": ["det-port", "model-port", "runtime"]} ]}, {"id": "cobolrt.JsonGenerate", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java", "variants": ["det-batch", "det-standalone", "sort-job"], "supers": [], "visibility": "public", "evidence": ["emitter", "emitter-dynamic", "signature", "harness", "runtime"], "members": [ + {"id": "cobolrt.JsonGenerate.anonymous(Node)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["emitter-dynamic", "harness"]}, {"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"]}, diff --git a/docs/language_status/cobolrt_rules.json b/docs/language_status/cobolrt_rules.json index e1dbd42734..7fe3474bb4 100644 --- a/docs/language_status/cobolrt_rules.json +++ b/docs/language_status/cobolrt_rules.json @@ -117,6 +117,7 @@ "group", "array", "table", + "anonymous", "read", "invalid", "skip", diff --git a/docs/language_status/register/X76.md b/docs/language_status/register/X76.md index 89b34c6360..531950cd95 100644 --- a/docs/language_status/register/X76.md +++ b/docs/language_status/register/X76.md @@ -4,13 +4,13 @@ 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 [WHEN ...] | EVERY [NUMERIC | NONNUMERIC] WHEN ...] over DISPLAY alphanumeric, fixed-point numeric (DISPLAY, COMP, COMP-3, COMP-5) items and fixed OCCURS tables and OCCURS DEPENDING ON 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, an ODO table inside another table or with an out-of-range count, 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 literal | OMITTED] [SUPPRESS item [WHEN ...] | EVERY [NUMERIC | NONNUMERIC] WHEN ...] over DISPLAY alphanumeric, fixed-point numeric (DISPLAY, COMP, COMP-3, COMP-5) items and fixed OCCURS tables and OCCURS DEPENDING ON 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 on a member, an ODO table inside another table or with an out-of-range count, 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); 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). +- **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; NAME OF the source IS OMITTED (with PTF for APAR PH18641) generates an anonymous JSON object whose top-level parent name is not written, so `{"A":{"B":1}}` becomes `{"B":1}` (IBM allows OMITTED only on identifier-2 itself; the pages show no example, so the anonymous object is read as the source group's `{members}` alone, and OMITTED on any other item is refused); 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, CONVERTING, NAME OMITTED and JSON PARSE 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 (IBM shows no example text: the reading above is an assumption) and JSON PARSE 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. - **OCCURS DEPENDING ON (#7524).** IBM (JSON GENERATE, SUPPRESS examples and Operation): a table is an array of as many elements as its ODO object holds, and a table with none is `[]` (a zero-occurrence group table shows no members). `cobolrt/JsonGenerate.table` takes the count at run time. What IBM's pages do not state is refused or a runtime `Hole`: a count outside the clause's bounds (undefined), and an ODO table inside another table (refused by name). JSON PARSE still refuses every table. diff --git a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java index af55b68db6..d22c2636be 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java @@ -60,6 +60,28 @@ public static Node table(String name, int count, int low, String depending, Node return new Array(name, List.of(elements).subList(0, count)); } + /** + * NAME OF the source IS OMITTED (IBM: an anonymous JSON object, the top-level parent name not generated): the text is + * the root group's `{members}` alone. + */ + public static Node anonymous(Node root) { + return new Anonymous(root); + } + + private static final class Anonymous extends Node { + final Node root; + + Anonymous(Node root) { + super(root.name); + this.root = root; + } + + @Override + void value(StringBuilder out, Charset cs) { + root.value(out, cs); + } + } + /** A group: `"name":{members}` (an object, its members in storage order). */ public static Node group(String name, Node... members) { return new Group(name, List.of(members)); @@ -98,9 +120,14 @@ public static int generate(Field into, Node root, Charset cs) { */ // [rule RT-CBL-JSON-GENERATE] public static int tryGenerate(Field into, Node root, Charset cs) { - StringBuilder out = new StringBuilder("{"); - root.render(out, cs); - out.append('}'); + StringBuilder out = new StringBuilder(); + if (root instanceof Anonymous a) { + a.value(out, cs); + } else { + out.append('{'); + root.render(out, cs); + out.append('}'); + } byte[] b = out.toString().getBytes(StandardCharsets.US_ASCII); if (b.length > into.length()) { return -1; diff --git a/gitgalaxy/tools/cobol_to_java/det/gen.py b/gitgalaxy/tools/cobol_to_java/det/gen.py index 1699a52c53..55918fa971 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3248,7 +3248,16 @@ def json_tree(self, d: dict, cls: str, what: str, src_ref: E.Ref) -> str: DISPLAY alphanumeric / numeric items under their names (NAME OF literals applied, SUPPRESSed items left out). The oracle has no JSON statement, so the model is IBM's text alone (registers X76 / X77); what it does not define is refused by name.""" - names = {id(self.resolve(r)): lit for r, lit in d["names"]} + names = {id(self.resolve(r)): lit for r, lit in d["names"] if lit is not None} + # #7524: NAME OF x IS OMITTED (IBM: an anonymous JSON object, the top-level parent name not generated; x must be + # identifier-2). The last NAME specification of the same item wins. + omitted = False + for r, lit in d["names"]: + if self.resolve(r) is not self.resolve(src_ref): + if lit is None: + raise Untranslatable(f"{what}: NAME OF {r.name} IS OMITTED (IBM needs the source itself)") + else: + omitted = lit is None 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]] = {} @@ -3350,7 +3359,8 @@ def node(it: L.Item, top: bool, idx: tuple[int, ...]) -> str | None: ) return f"__PACKAGE__.cobolrt.{cls}.array({jstr(name)}{''.join(', ' + str(e) for e in elems)})" - return node(src, True, ()) or "" + root = node(src, True, ()) or "" + return f"__PACKAGE__.cobolrt.{cls}.anonymous({root})" if omitted else root @staticmethod def json_when_fits(it: L.Item, value: str) -> bool: diff --git a/gitgalaxy/tools/cobol_to_java/det/stmt.py b/gitgalaxy/tools/cobol_to_java/det/stmt.py index 9d87d3dbed..df8bccea56 100644 --- a/gitgalaxy/tools/cobol_to_java/det/stmt.py +++ b/gitgalaxy/tools/cobol_to_java/det/stmt.py @@ -1459,14 +1459,19 @@ def starts(k: int) -> bool: _JSON_PHRASES = ("COUNT", "NAME", "SUPPRESS", "END-JSON", "WITH") -def _json_name_phrase(p: E.Parser, names: list) -> None: - """NAME [OF] item [IS] literal ...: appends (item, literal text); anything but an alphanumeric literal raises.""" +def _json_name_phrase(p: E.Parser, names: list, omitted_ok: bool = False) -> None: + """NAME [OF] item [IS] literal ...: appends (item, literal text); anything but an alphanumeric literal raises. + #7524: `IS OMITTED` (JSON GENERATE only, `omitted_ok`) appends (item, None).""" p.take() p.accept("OF") start = len(names) while not p.done() and p.up() not in _JSON_PHRASES: item = p.ref() p.accept("IS") + if omitted_ok and p.up() == "OMITTED": + p.take() + names.append((item, None)) + continue lit = p.operand() if not (isinstance(lit, E.Lit) and isinstance(lit.value, str)): raise E.ExprError("NAME needs an alphanumeric literal") @@ -1527,9 +1532,9 @@ def _json_suppress_phrase(p: E.Parser, suppress: list, when: list | None = 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], when [(item, [figurative])] (SUPPRESS item + data: into, src, count, names [(item, literal text | None for OMITTED)], 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) + on the statement (CONVERTING, 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() @@ -1548,7 +1553,7 @@ def _json_generate(text: str, line: int) -> Stmt: p.accept("IN") count = p.ref() elif u == "NAME": - _json_name_phrase(p, names) + _json_name_phrase(p, names, True) elif u == "SUPPRESS": _json_suppress_phrase(p, suppress, when, generic) elif u == "END-JSON": diff --git a/tests/cobol_mainframe/test_det_json_generate.py b/tests/cobol_mainframe/test_det_json_generate.py index 05a147636c..e480161874 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -74,7 +74,6 @@ def test_name_of_and_suppress_shape_the_members(tmp_path): "JSON GENERATE OUT FROM REC CONVERTING ON EXCEPTION\n DISPLAY 'X'\n END-JSON", "CONVERTING phrase", ), - ("JSON GENERATE OUT FROM REC NAME OF AMT IS OMITTED", "NAME needs an alphanumeric literal"), ], ) def test_a_shape_the_model_does_not_take_is_refused_by_name(tmp_path, stmt, why): @@ -128,7 +127,7 @@ def test_phrases_scope_to_their_own_statement(tmp_path): " END-JSON", " DISPLAY 'AFTER'", "END-IF", - "JSON GENERATE OUT FROM REC NAME OF AMT IS OMITTED", + "JSON GENERATE OUT FROM REC NAME OF AMT IS 5", " ON EXCEPTION MOVE 2 TO N", "END-JSON", ) @@ -140,6 +139,28 @@ def test_xml_and_parse_phrases_stay_in_their_hole(tmp_path): assert len(r.stats["holes"]) == 1 and "XML GENERATE" in str(r.stats["holes"][0]) +def test_name_omitted_on_the_source_makes_an_anonymous_object(tmp_path): + """#7524: IBM's NAME phrase: OMITTED generates an anonymous JSON object (identifier-4 must be identifier-2).""" + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC NAME OF REC IS 'x'", " NAME OF REC IS OMITTED") + assert r.stats["holes"] == [] + assert "JsonGenerate.anonymous(" in r.java + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC NAME OF REC IS OMITTED", " NAME OF REC IS 'x'") + assert "anonymous(" not in r.java and 'group("x"' in r.java # the last specification wins + + +def test_name_omitted_on_a_member_is_refused_by_name(tmp_path): + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC NAME OF AMT IS OMITTED") + assert len(r.stats["holes"]) == 1 + assert "NAME OF AMT IS OMITTED (IBM needs the source itself)" in str(r.stats["holes"][0]) + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC NAME OF REC IS OMITTED", " SUPPRESS REC") + assert r.stats["holes"] == [] # the source group is always written + + +def test_name_omitted_is_parsed_for_generate_only(tmp_path): + st = S._json_generate("JSON GENERATE A FROM R NAME OF R IS OMITTED END-JSON", 1) + assert st.data["names"][0][1] is None + + def test_an_elementary_source_is_refused_by_name(tmp_path): r = _translate(tmp_path, "JSON GENERATE OUT FROM CUST-NAME") assert len(r.stats["holes"]) == 1 and "JSON GENERATE: CUST-NAME is not a plain group" in r.stats["holes"][0] @@ -195,6 +216,17 @@ def test_the_runtime_writes_ibms_text_and_stops_where_ibm_is_silent(tmp_path): run("junk", "Ann ", 3, 0, false, "0a7", 40); run("wide", "\\u00e9 ", 3, 0, false, "007", 40); run("negzero", "Ann ", 3, 0, true, "00}", 40); + anon(); + } + static void anon() { + Storage st = new Storage(200); + byte[] t = "Ann ".getBytes(CS); + System.arraycopy(t, 0, st.bytes, 0, t.length); + Field out = Field.alphanumeric(st, 100, 40, false); + Field a = Field.alphanumeric(st, 0, 8, false); + JsonGenerate.Node g = JsonGenerate.group("G", JsonGenerate.text("A-B", a), JsonGenerate.group("H")); // an all-ignored group is left out + System.out.println("anon " + JsonGenerate.generate(out, JsonGenerate.anonymous(g), CS) + " " + + new String(st.bytes, 100, 40, CS).trim()); } } """ @@ -215,6 +247,7 @@ def test_the_runtime_writes_ibms_text_and_stops_where_ibm_is_silent(tmp_path): assert out[5].startswith("small refused: JSON GENERATE: the JSON is longer than the receiver of 24 bytes") assert "non-digit data in numeric item N" in out[6] and "undefined by IBM" in out[6] assert "beyond ASCII" in out[7] and "negative zero" in out[8] + assert out[9] == 'anon 13 {"A-B":"Ann"}' # OMITTED: the source group's own name is not written WIDE = """ IDENTIFICATION DIVISION.