diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 1e734695c..22402c087 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": 807, "members_internal": 391}, + "summary": {"types": 128, "types_public": 105, "members": 1199, "members_public": 808, "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": []}, @@ -1011,6 +1011,7 @@ {"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,String...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "internal", "evidence": ["harness"]}, + {"id": "cobolrt.JsonGenerate.table(String,int,int,String,Node...)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "evidence": ["emitter-dynamic", "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"]} ]}, diff --git a/docs/language_status/cobolrt_rules.json b/docs/language_status/cobolrt_rules.json index 1cde927d6..e1dbd4273 100644 --- a/docs/language_status/cobolrt_rules.json +++ b/docs/language_status/cobolrt_rules.json @@ -116,6 +116,7 @@ "plumbing": [ "group", "array", + "table", "read", "invalid", "skip", diff --git a/docs/language_status/register/X76.md b/docs/language_status/register/X76.md index bdb50801f..89b34c636 100644 --- a/docs/language_status/register/X76.md +++ b/docs/language_status/register/X76.md @@ -4,12 +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 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" +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" 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). - **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 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 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 bfa36e662..af55b68db 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java @@ -48,6 +48,18 @@ public static Node array(String name, Node... elements) { return new Array(name, List.of(elements)); } + /** + * A table with OCCURS ... DEPENDING ON: the first `count` of the `elements` (one per occurrence up to the clause's + * maximum), `[]` for none. A count outside `low` to the maximum is undefined by IBM: a {@link Hole}. + */ + public static Node table(String name, int count, int low, String depending, Node... elements) { + if (count < low || count > elements.length) { + throw new Hole("JSON GENERATE: " + depending + " holds " + count + ", outside " + low + " to " + elements.length + + " of the table " + name + "; IBM leaves it undefined"); + } + return new Array(name, List.of(elements).subList(0, count)); + } + /** 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)); diff --git a/gitgalaxy/tools/cobol_to_java/det/gen.py b/gitgalaxy/tools/cobol_to_java/det/gen.py index 27ce768c6..5523248db 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3311,10 +3311,11 @@ 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).""" if it.name.upper() == "FILLER" or it.redefines: return None - if it.depending or (it.occurs != 1 and (not wide or top)): - raise Untranslatable( - f"{what}: {it.name} is a table (OCCURS{' DEPENDING ON' if it.depending else ''} is not modelled)" - ) + if it.occurs != 1 and (not wide or top): + raise Untranslatable(f"{what}: {it.name} is a table (OCCURS is not modelled)") + if it.depending and idx: + # IBM's pages do not say how a variable-length table inside another table is generated: refused + raise Untranslatable(f"{what}: {it.name} is OCCURS DEPENDING ON inside another table (not modelled)") if id(it) in suppressed and not top: return None name = names.get(id(it), it.name) @@ -3338,6 +3339,15 @@ def node(it: L.Item, top: bool, idx: tuple[int, ...]) -> str | None: if None in elems: return None seen.add(name) + if it.depending: + # IBM (JSON GENERATE): a table is generated with the occurrences its ODO object holds, none as `[]`. A + # value outside the clause's bounds is not stated by IBM (undefined): a runtime Hole. + cnt = self.int_expr(E.Parser(E.tokenize(it.depending)).ref()) + low = it.occurs_min if it.occurs_min is not None else 0 + return ( + f"__PACKAGE__.cobolrt.{cls}.table({jstr(name)}, {cnt}, {low}, {jstr(it.depending)}" + f"{''.join(', ' + str(e) for e in elems)})" + ) return f"__PACKAGE__.cobolrt.{cls}.array({jstr(name)}{''.join(', ' + str(e) for e in elems)})" return node(src, True, ()) or "" diff --git a/tests/cobol_mainframe/test_det_json_generate.py b/tests/cobol_mainframe/test_det_json_generate.py index e11a2ddf7..997f375c2 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -263,7 +263,7 @@ def test_name_of_and_suppress_apply_to_a_table_and_its_elements(tmp_path): @pytest.mark.parametrize( ("data_line", "why"), [ - (" 05 T PIC X OCCURS 1 TO 5 DEPENDING ON N1.", "OCCURS DEPENDING ON is not modelled"), + (" 05 T PIC X OCCURS 1 TO 5 DEPENDING ON N1.", None), (" 05 T PIC X OCCURS 300.", "more than 256 items and table elements"), (" 05 T COMP-1.", "COMP-1 FLOAT"), (" 05 T PIC S9(4) SIGN LEADING SEPARATE.", "P-scaled or separately signed"), @@ -273,12 +273,40 @@ def test_name_of_and_suppress_apply_to_a_table_and_its_elements(tmp_path): def test_wide_shapes_the_model_does_not_take_are_refused_by_name(tmp_path, data_line, why): data = WIDE.replace(" 01 OUT", " 05 N1 PIC 9.\n" + data_line + "\n 01 OUT") r = _wide(tmp_path, "JSON GENERATE OUT FROM REC", data=data) - if why is None: # a REDEFINES item is left out, as IBM says + if why is None: # a REDEFINES item is left out, as IBM says; an ODO table is translated (see below) assert r.stats["holes"] == [] return assert len(r.stats["holes"]) == 1 and why in str(r.stats["holes"][0]) +ODO_DATA = WIDE.replace( + " 01 OUT", + " 05 N1 PIC 9.\n 05 VT PIC X(2) OCCURS 0 TO 4 DEPENDING ON N1.\n" + " 05 VG OCCURS 1 TO 3 DEPENDING ON N1.\n 10 VQ PIC 9(2).\n 01 OUT", +) + + +def test_an_odo_table_is_an_array_of_the_odo_objects_count(tmp_path): + """#7524: IBM's LR: a table with zero occurrences is `[]`; the elements generated are the ODO object's count.""" + r = _wide(tmp_path, "JSON GENERATE OUT FROM REC NAME OF VT IS 'v'", data=ODO_DATA) + java = str(r.java) + assert r.stats["holes"] == [] + assert 'JsonGenerate.table("v", ' in java and 'JsonGenerate.table("VG", ' in java + assert java.count('"N1"') >= 1 and ', 0, "N1"' in java and ', 1, "N1"' in java # the clause's lower bounds + + +def test_an_odo_table_inside_another_table_is_refused(tmp_path): + line = " 05 TT OCCURS 2.\n 10 IN-T PIC X OCCURS 1 TO 3 DEPENDING ON N1." + data = WIDE.replace(" 01 OUT", " 05 N1 PIC 9.\n" + line + "\n 01 OUT") + r = _wide(tmp_path, "JSON GENERATE OUT FROM REC", data=data) + assert len(r.stats["holes"]) == 1 and "OCCURS DEPENDING ON inside another table" in str(r.stats["holes"][0]) + + +def test_parse_still_refuses_an_odo_table(tmp_path): + r = _wide(tmp_path, "JSON PARSE OUT INTO REC", data=ODO_DATA) + assert len(r.stats["holes"]) == 1 and "table" in str(r.stats["holes"][0]) + + def test_a_table_as_the_source_itself_is_still_refused(tmp_path): r = _wide(tmp_path, "JSON GENERATE OUT FROM LINES-T") assert len(r.stats["holes"]) == 1 and "table" in str(r.stats["holes"][0]) @@ -315,6 +343,13 @@ def test_the_runtime_writes_comp_numbers_and_tables(tmp_path): System.out.println(what + " refused: " + e.getMessage()); } } + static void showS(String what, Field out, Storage st, java.util.function.Supplier n) { + try { + show(what, out, st, n.get()); + } catch (RuntimeException e) { + System.out.println(what + " refused: " + e.getMessage()); + } + } static Storage st(int... b) { Storage s = new Storage(200); for (int i = 0; i < b.length; i++) s.bytes[i] = (byte) b[i]; @@ -330,6 +365,12 @@ def test_the_runtime_writes_comp_numbers_and_tables(tmp_path): show("table", out, s, JsonGenerate.group("G", JsonGenerate.array("T", JsonGenerate.text("", Field.alphanumeric(s, 11, 2, false)), JsonGenerate.text("", Field.alphanumeric(s, 13, 2, false))), JsonGenerate.array("U", JsonGenerate.group("", JsonGenerate.number("Q", Field.packed(s, 9, 3, 0, false)))))); + Field e0 = Field.alphanumeric(s, 11, 2, false); + Field e1 = Field.alphanumeric(s, 13, 2, false); + show("odo2", out, s, JsonGenerate.group("G", JsonGenerate.table("T", 2, 0, "N", JsonGenerate.text("", e0), JsonGenerate.text("", e1)))); + show("odo0", out, s, JsonGenerate.group("G", JsonGenerate.table("T", 0, 0, "N", JsonGenerate.text("", e0), JsonGenerate.text("", e1)))); + showS("odohi", out, s, () -> JsonGenerate.table("T", 3, 0, "N", JsonGenerate.text("", e0), JsonGenerate.text("", e1))); + showS("odolo", out, s, () -> JsonGenerate.table("T", 0, 1, "N", JsonGenerate.text("", e0))); Storage bad = st(0x1A, 0x2C, 0x12, 0x34, 0x0D, 0x00, 0x1D); Field o2 = Field.alphanumeric(bad, 100, 100, false); show("badnibble", o2, bad, JsonGenerate.number("P", Field.packed(bad, 0, 3, 0, true))); @@ -351,6 +392,9 @@ def test_the_runtime_writes_comp_numbers_and_tables(tmp_path): assert out[0] == 'bin {"B":-123}' and out[1] == 'packed {"P":-123.45}' assert out[2] == 'native {"N":42}' and out[3] == 'pd {"D":12}' assert '"T":["a","b"]' in out[4] and '"U":[{"Q":12}]' in out[4] + assert out[5] == 'odo2 {"G":{"T":["a","b"]}}' and out[6] == 'odo0 {"G":{"T":[]}}' + assert "outside 0 to 2" in out[7] and "outside 1 to 1" in out[8] and "undefined" in out[7] + out = out[:5] + out[9:] assert "non-digit data in numeric item P" in out[5] and "non-digit data in numeric item P" in out[6] assert "negative zero" in out[7] and "past its PICTURE's digits" in out[8]