From cec63e440f5fda7ed2a448f9ac330bdd89854c58 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 11:31:07 -0500 Subject: [PATCH 1/3] det: JSON GENERATE takes OCCURS DEPENDING ON tables (Part of #7524) Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/cobolrt_api.json | 3 +- docs/language_status/register/X76.md | 5 +- .../det/cobolrt/JsonGenerate.java | 12 +++++ gitgalaxy/tools/cobol_to_java/det/gen.py | 18 +++++-- .../cobol_mainframe/test_det_json_generate.py | 48 ++++++++++++++++++- 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 0bb5ce166..c9aed83e0 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": 1199, "members_public": 810, "members_internal": 389}, "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)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "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)", "kind": "method", "static": true, "returns": "cobolrt.JsonGenerate.Node", "visibility": "public", "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/register/X76.md b/docs/language_status/register/X76.md index 8aff663e2..3fa98b5fe 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] 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, SUPPRESS WHEN / EVERY, 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); 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, SUPPRESS WHEN / EVERY and JSON PARSE tables are later slices of #7524. - **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 30f13bd3a..fe0017844 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonGenerate.java @@ -43,6 +43,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 cfdf1922d..7a1b8646c 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3295,10 +3295,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) @@ -3322,6 +3323,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 aca132e6e..27b4252cc 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -261,7 +261,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"), @@ -271,12 +271,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]) @@ -313,6 +341,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]; @@ -328,6 +363,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))); @@ -349,6 +390,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] From 61559e1512430467bb854657ca66b6c07d5615d2 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 11:35:49 -0500 Subject: [PATCH 2/3] cobolrt rules: JsonGenerate.table is plumbing (Part of #7524) Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/cobolrt_rules.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/language_status/cobolrt_rules.json b/docs/language_status/cobolrt_rules.json index abce0d6a7..40d8a0f31 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", "text", From 95ced4cecb8266fe59e94544fee63d196fe2178b Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 12:07:00 -0500 Subject: [PATCH 3/3] cobolrt api: JsonGenerate.table is used by emitted code, so public (Part of #7524) Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/cobolrt_api.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index bccb05dc0..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": 1199, "members_public": 807, "members_internal": 392}, + "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,7 +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": "internal", "evidence": ["emitter-dynamic", "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"]} ]},