From 084c822b877fc690add423b56af1832a9e4e32c5 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 10:10:40 -0500 Subject: [PATCH 1/3] det: JSON PARSE sets JSON-CODE from IBM's exception table (Part of #7524, #7709) Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/register/X77.md | 6 +- .../cobol_to_java/det/cobolrt/JsonParse.java | 109 ++++++++++++++---- gitgalaxy/tools/cobol_to_java/det/gen.py | 55 +++++---- .../cobol_mainframe/test_det_json_generate.py | 16 ++- tests/cobol_mainframe/test_det_json_parse.py | 95 ++++++++++----- 5 files changed, 190 insertions(+), 91 deletions(-) diff --git a/docs/language_status/register/X77.md b/docs/language_status/register/X77.md index 5fb3beaa8..6c9aa371e 100644 --- a/docs/language_status/register/X77.md +++ b/docs/language_status/register/X77.md @@ -4,11 +4,11 @@ family: "X" status: "ASSUMED" title: "JSON PARSE into a group of DISPLAY, COMP, COMP-3 and COMP-5 items is translated from IBM's text alone — ASSUMED, oracle-unbacked (#7524)" area: "COBOL" -summary: "JSON PARSE source [WITH DETAIL] INTO group [NAME OF ... IS] [SUPPRESS item] over DISPLAY alphanumeric text and fixed-point numbers (DISPLAY, COMP, COMP-3 / PACKED-DECIMAL, COMP-5) is generated as a runtime call that reads one JSON object and stores its members by name. GnuCOBOL has no JSON statement, so nothing backs it: no L2+ claim. ON EXCEPTION / NOT ON EXCEPTION are modelled for malformed text, a non-object, a type mismatch, a string longer than its item and a number that does not fit (the exception condition). JSON-CODE / JSON-STATUS, SUPPRESS WHEN / EVERY, CONVERTING, NAME OMITTED, tables, float items, an elementary receiver and any character beyond ASCII stay holes or runtime Holes" +summary: "JSON PARSE source [WITH DETAIL] INTO group [NAME OF ... IS] [SUPPRESS item] over DISPLAY alphanumeric text and fixed-point numbers (DISPLAY, COMP, COMP-3 / PACKED-DECIMAL, COMP-5) is generated as a runtime call that reads one JSON object and stores its members by name. GnuCOBOL has no JSON statement, so nothing backs it: no L2+ claim. ON EXCEPTION / NOT ON EXCEPTION are modelled for malformed text, a non-object, a type mismatch, empty text, text after the closing brace and no name matching (the exception condition). JSON-CODE after an exception (100 invalid text, 101 empty, 102 text after the closing brace, 104 incompatible value, 106 no name matched) is set from IBM's Programming Guide table; JSON-STATUS is not set (a program naming it is not translated); SUPPRESS WHEN / EVERY, CONVERTING, NAME OMITTED, tables, float items, an elementary receiver and any character beyond ASCII stay holes or runtime Holes" reached: "no (no burned program uses JSON; fixture tests/cobol_mainframe/test_det_json_parse.py)" --- - **What IBM says** (Enterprise COBOL for z/OS 6.3 Language Reference, JSON PARSE: https://www.ibm.com/docs/SS6SG3_6.3.0/lr/ref/rlpsjsop.html and its sub-pages). The members of the JSON object are matched by name to the subordinate items of the receiving group; a JSON name with no data item is ignored and an item with no JSON member is left unchanged; NAME OF renames an item, SUPPRESS leaves one out; a string goes to an alphanumeric item (left-justified, padded with spaces) and a number to a numeric item (aligned on the decimal point); WITH DETAIL only widens what the exception condition reports. A text that is not valid JSON, or a value that does not fit or has the wrong type, raises the exception condition (JSON-CODE nonzero, ON EXCEPTION). - **Modelled.** `det/stmt.py` `_json_parse` parses the statement, `det/gen.py` `json_parse` builds the receiving tree (shared with JSON GENERATE through `json_tree`) and refuses the shapes below by name, `cobolrt/JsonParse.java` reads and stores. A statement it cannot take keeps the `JSON PARSE (...): not modelled` hole text, with the reason for the shape appended. -- **What we assume.** (1) The text is one JSON object whose members map to the group's subordinates; the group's own name is not in the text (unlike the GENERATE wrapper). (2) Names match case-sensitively and exactly. (3) The exception condition (#7524): with ON EXCEPTION / NOT ON EXCEPTION, malformed text, text that is not a JSON object, a value of the wrong type, a string longer than its item and a number too large for its item raise it (ON EXCEPTION runs, else NOT ON EXCEPTION); without the phrases each is a runtime `Hole`. Malformed text stores nothing; after a type or size failure the items matched before it keep the values stored (assumed: IBM's text is read as leaving the receiver unspecified, the model stores left to right). A number with more decimals than the item, a negative number for an unsigned item, an exponent, a repeated name in one object and any character beyond ASCII are a `Hole` even with the phrases, never a guess. JSON-CODE / JSON-STATUS are not set. (4) WITH DETAIL changes nothing the model reports, since only the exception condition uses it. +- **What we assume.** (1) The text is one JSON object whose members map to the group's subordinates; the group's own name is not in the text (unlike the GENERATE wrapper). (2) Names match case-sensitively and exactly. (3) The exception condition (#7524): with ON EXCEPTION / NOT ON EXCEPTION, malformed text, text that is not a JSON object, a value of the wrong type, empty text, text after the closing brace and no name matching raise it (ON EXCEPTION runs, else NOT ON EXCEPTION); without the phrases each is a runtime `Hole`. Malformed text and text after the closing brace store nothing; after a type or size failure the items matched before it keep the values stored (assumed: IBM's text is read as leaving the receiver unspecified, the model stores left to right). A string longer than its item and a number too large for it are nonexception in IBM (JSON-STATUS 256 / 128, stored anyway, in a form the text does not state), so they are a `Hole`; so are a JSON array, `true`, `false`, `null`, a non-object top level (no JSON-CODE stated), and JSON-STATUS after an exception. A number with more decimals than the item, a negative number for an unsigned item, an exponent, a repeated name in one object and any character beyond ASCII are a `Hole` even with the phrases, never a guess. JSON-CODE (S9(9) BINARY) is the exception code of the Programming Guide's Table 109 and 0 on success; The runtime computes JSON-STATUS bits 1 (an item with no pair) and 2 (a pair with no item) (Table 108) but the translator does not yet store them. A program's own JSON-CODE item is kept. The hidden JSON-CODE register keeps its S9(4) width (IBM's is S9(9) BINARY; the width shows only through DISPLAY). (4) WITH DETAIL changes nothing the model reports, since only the exception condition uses it. - **Packed and binary numbers (#7524, #7709).** A COMP / COMP-3 / COMP-5 numeric receiver takes the number by the same rules as a DISPLAY one (decimal alignment, no extra decimals, no negative for an unsigned item, a value past the PICTURE digits is the exception condition); the store is the runtime's `Cobol.store`, IBM's text states no other byte layout. A table receiver stays refused for PARSE. -- **Status:** ASSUMED, **oracle-unbacked**; a port using it is reported as assumed (IBM-documented). JSON-CODE / JSON-STATUS for both verbs, SUPPRESS WHEN / EVERY, CONVERTING and the wider item kinds are later slices of #7524. +- **Status:** ASSUMED, **oracle-unbacked**; a port using it is reported as assumed (IBM-documented). JSON-STATUS bits 4-512, JSON-STATUS after an exception, SUPPRESS WHEN / EVERY, CONVERTING and the wider item kinds are later slices of #7524. diff --git a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java index 1a1b1958a..9ec373233 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java @@ -17,7 +17,11 @@ * matched by name (case-sensitive) to the group's subordinate items; a name with no target is ignored and a target with * no member keeps its value. A string goes to an alphanumeric item (left-justified, space padded), a number to a * fixed-point DISPLAY item. What IBM answers with the exception condition (JSON-CODE, ON EXCEPTION: malformed text, a - * value that does not fit or has the wrong type) is a {@link Hole} unless the statement has ON EXCEPTION / NOT ON EXCEPTION (tryParse). + * value of the wrong type, no name matching any item) is a {@link Hole} unless the statement has ON EXCEPTION / + * NOT ON EXCEPTION or names JSON-CODE / JSON-STATUS (the six-argument tryParse). The codes are IBM's (Programming Guide, Appendix D, + * Table 109: 100 invalid text, 101 empty text, 102 text after the closing brace, 104 incompatible value, 106 no name + * matched) and the JSON-STATUS bits are Table 108 (1 an item had no pair, 2 a pair had no item). A value that does not + * fit (JSON-STATUS 128, 256: IBM stores it anyway, in a form the text does not state) is a Hole. */ public final class JsonParse { private JsonParse() { @@ -31,7 +35,13 @@ public abstract static class Node { this.name = name; } - abstract void accept(Object value); + abstract void accept(Object value, Ctx c); + } + + /** What one statement collects: the JSON-STATUS bits and how many JSON pairs matched an item. */ + private static final class Ctx { + int status; + int matched; } /** A group: members matched by name against a JSON object. */ @@ -51,8 +61,12 @@ public static Node number(String name, Field f) { /** The exception condition of IBM's definition (JSON-CODE nonzero): caught by tryParse, else a Hole. */ private static final class JsonError extends Hole { - JsonError(String what) { + /** IBM's JSON-CODE for this exception, or -1 when the Programming Guide does not state it for this shape. */ + final int code; + + JsonError(String what, int code) { super(what); + this.code = code; } } @@ -64,17 +78,38 @@ private static final class JsonError extends Hole { */ // [rule RT-CBL-JSON-PARSE] public static boolean tryParse(Field src, Node root, Charset cs) { + return tryParse(src, root, cs, new int[1], false, false) != 0; + } + + /** + * As the three-argument tryParse, returning IBM's JSON-CODE (0 after a success) and setting status[0] to JSON-STATUS. needCode: the + * program reads JSON-CODE, so an exception whose code IBM does not state is a Hole; needStatus: the program reads + * JSON-STATUS, whose value after an exception (bits for what was seen before it) is not modelled, so an exception is + * a Hole. Without them the exception is still reported (return -1 for an unstated code). + */ + // [rule RT-CBL-JSON-PARSE] + public static int tryParse(Field src, Node root, Charset cs, int[] status, boolean needCode, boolean needStatus) { try { - parse(src, root, cs); - return false; + parse(src, root, cs, status); + return 0; } catch (JsonError e) { - return true; + if (needStatus) { + throw new Hole("JSON PARSE: JSON-STATUS after an exception is not modelled (" + e.getMessage() + ")"); + } + if (e.code < 0 && needCode) { + throw new Hole(e.getMessage()); + } + return e.code; } } /** Parses the JSON text held in `src` (trailing spaces ignored) and stores its members into `root`'s items. */ // [rule RT-CBL-JSON-PARSE] public static void parse(Field src, Node root, Charset cs) { + parse(src, root, cs, new int[1]); + } + + private static void parse(Field src, Node root, Charset cs, int[] status) { String t = Cobol.text(src, cs); for (int i = 0; i < t.length(); i++) { if (t.charAt(i) > 0x7F) { @@ -84,16 +119,24 @@ public static void parse(Field src, Node root, Charset cs) { } Reader r = new Reader(t); r.ws(); + if (r.i >= t.length()) { + throw new JsonError("JSON PARSE: the text is empty or only white space (JSON-CODE 101)", 101); + } Object v = r.value(); r.ws(); - if (r.i != t.length() && !t.substring(r.i).isBlank()) { - throw new JsonError("JSON PARSE: text after the JSON value; IBM raises the exception condition (JSON-CODE), not modelled"); - } if (!(v instanceof Members)) { - throw new JsonError("JSON PARSE: the text is not a JSON object; IBM raises the exception condition, not modelled"); + throw new JsonError("JSON PARSE: the text is not a JSON object; the Programming Guide states no JSON-CODE for it", -1); + } + if (r.i != t.length() && !t.substring(r.i).isBlank()) { + throw new JsonError("JSON PARSE: text after the closing brace of the outermost object (JSON-CODE 102)", 102); } // the members of the text are the subordinates of the receiving group; the group's own name is not in the text - ((Group) root).fill((Members) v); + Ctx c = new Ctx(); + ((Group) root).fill((Members) v, c); + if (c.matched == 0) { + throw new JsonError("JSON PARSE: no JSON name matched any data item (JSON-CODE 106)", 106); + } + status[0] = c.status; } /** A parsed JSON object: names in text order. */ @@ -110,28 +153,39 @@ private static final class Group extends Node { this.members = members; } - void fill(Members m) { + void fill(Members m, Ctx c) { Set seen = new HashSet<>(); for (int i = 0; i < m.names.size(); i++) { if (!seen.add(m.names.get(i))) { throw new Hole("JSON PARSE: the name " + m.names.get(i) + " occurs twice in one object; not modelled"); } } + Set hit = new HashSet<>(); for (int i = 0; i < m.names.size(); i++) { + boolean found = false; for (Node n : members) { if (n.name.equals(m.names.get(i))) { - n.accept(m.values.get(i)); + found = true; + hit.add(n); + c.matched++; + n.accept(m.values.get(i), c); } } + if (!found) { + c.status |= 2; // JSON-STATUS 2: a JSON pair matched no data item + } + } + if (hit.size() < members.size()) { + c.status |= 1; // JSON-STATUS 1: a data item had no matching pair and was not changed } } @Override - void accept(Object value) { + void accept(Object value, Ctx c) { if (!(value instanceof Members)) { - throw new JsonError("JSON PARSE: " + name + " is a group but the JSON value is not an object; IBM raises the exception condition, not modelled"); + throw new JsonError("JSON PARSE: " + name + " is a group but the JSON value is not an object (JSON-CODE 104)", 104); } - fill((Members) value); + fill((Members) value, c); } } @@ -144,14 +198,15 @@ private static final class Text extends Node { } @Override - void accept(Object value) { + void accept(Object value, Ctx c) { if (!(value instanceof String)) { - throw new JsonError("JSON PARSE: " + name + " takes a JSON string; IBM raises the exception condition for another type, not modelled"); + throw new JsonError("JSON PARSE: " + name + " takes a JSON string, the value is another type (JSON-CODE 104)", 104); } byte[] b = ((String) value).getBytes(StandardCharsets.US_ASCII); if (b.length > f.length()) { - throw new JsonError("JSON PARSE: " + b.length + " characters for " + name + " of " + f.length() - + "; IBM raises the exception condition, not modelled"); + // JSON-STATUS 256: IBM stores the value anyway ("loss of information"); the stored form is not stated + throw new Hole("JSON PARSE: " + b.length + " characters for " + name + " of " + f.length() + + "; IBM's assignment on a loss of information (JSON-STATUS 256) is not modelled"); } byte[] out = new byte[f.length()]; java.util.Arrays.fill(out, (byte) ' '); @@ -169,9 +224,9 @@ private static final class Num extends Node { } @Override - void accept(Object value) { + void accept(Object value, Ctx c) { if (!(value instanceof BigDecimal)) { - throw new JsonError("JSON PARSE: " + name + " takes a JSON number; IBM raises the exception condition for another type, not modelled"); + throw new JsonError("JSON PARSE: " + name + " takes a JSON number, the value is another type (JSON-CODE 104)", 104); } BigDecimal v = (BigDecimal) value; if (v.scale() > f.scale() && v.stripTrailingZeros().scale() > f.scale()) { @@ -182,7 +237,8 @@ void accept(Object value) { throw new Hole("JSON PARSE: a negative number for the unsigned item " + name + "; not modelled"); } if (v.precision() - v.scale() > f.digits() - f.scale() && v.signum() != 0) { - throw new JsonError("JSON PARSE: the number does not fit " + name + "; IBM raises the exception condition, not modelled"); + // JSON-STATUS 128: a SIZE ERROR; IBM stores the value anyway, in a form the text does not state + throw new Hole("JSON PARSE: the number does not fit " + name + "; IBM's assignment on a SIZE ERROR (JSON-STATUS 128) is not modelled"); } Cobol.store(f, v, false, StandardCharsets.ISO_8859_1); } @@ -204,7 +260,7 @@ void ws() { } Hole bad(String what) { - return new JsonError("JSON PARSE: " + what + " at offset " + i + "; IBM raises the exception condition (JSON-CODE), not modelled"); + return new JsonError("JSON PARSE: " + what + " at offset " + i + " (JSON-CODE 100)", 100); } Object value() { @@ -221,7 +277,10 @@ Object value() { if (c == '-' || (c >= '0' && c <= '9')) { return number(); } - throw bad("a JSON array, true, false, null or malformed value (not modelled)"); + if (c == '[' || s.startsWith("true", i) || s.startsWith("false", i) || s.startsWith("null", i)) { + throw new Hole("JSON PARSE: a JSON array, true, false or null at offset " + i + " is not modelled"); + } + throw bad("a malformed value"); } Members object() { diff --git a/gitgalaxy/tools/cobol_to_java/det/gen.py b/gitgalaxy/tools/cobol_to_java/det/gen.py index cfdf1922d..81afb1d58 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3332,7 +3332,7 @@ def json_generate(self, s: S.Stmt, ind: str) -> list[str]: root = self.json_tree(d, "JsonGenerate", "JSON GENERATE", d["src"]) into = self.field_expr(d["into"]) call = f"__PACKAGE__.cobolrt.JsonGenerate.generate({into}, {root}, CS)" - reg = self.json_code_store(s, "JSON GENERATE", ind) + reg = self.json_code_store(s, ind) if "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases: # IBM: a receiver too small raises the exception condition (ON EXCEPTION runs, else NOT ON EXCEPTION). The # receiver and COUNT are undefined then; the port leaves them as they were. @@ -3351,34 +3351,45 @@ def json_generate(self, s: S.Stmt, ind: str) -> list[str]: n = self.field_expr(d["count"]) return [f"{ind}Cobol.store({n}, BigDecimal.valueOf({call}), false, CS);", *reg] - def json_code_store(self, s: S.Stmt, what: str, ind: str) -> list[str]: - """#7524: the lines that set JSON-CODE to 0 after a JSON GENERATE that raised no exception (IBM LR: "contains zero - after execution"); [] when the program does not name JSON-CODE. The code IBM sets for an exception sits in the - Programming Guide's exception table, which the model does not state, so a program that names JSON-CODE and could - take an exception (an ON EXCEPTION phrase, or any JSON PARSE) is refused by name.""" - if not any(k.startswith("GG-JSON") for k in self.items): + def json_code_store(self, s: S.Stmt, ind: str) -> list[str]: + """#7524: the line that sets JSON-CODE to 0 after a JSON GENERATE that raised no exception (IBM LR: "contains zero + after execution"); [] when the program does not name JSON-CODE. The code a GENERATE exception sets (1, a receiver + too small: Programming Guide Table 107) is not modelled yet, so a program that names JSON-CODE and has an ON + EXCEPTION phrase is refused by name.""" + if "GG-JSON-CODE" not in self.items: return [] - if what != "JSON GENERATE" or "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases: - raise Untranslatable(f"{what}: the JSON-CODE value after an exception is not modelled") + if "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases: + raise Untranslatable("JSON GENERATE: the JSON-CODE value after an exception is not modelled") return [f"{ind}Cobol.store({self.field_expr(E.Ref('JSON-CODE'))}, BigDecimal.ZERO, false, CS);"] def json_parse(self, s: S.Stmt, ind: str) -> list[str]: - """#7524: JSON PARSE of an alphanumeric item INTO a group of DISPLAY alphanumeric / numeric items (X77).""" + """#7524: JSON PARSE of an alphanumeric item INTO a group of DISPLAY alphanumeric / numeric items (X77). + JSON-CODE is IBM's exception code (Programming Guide, Table 109) and JSON-STATUS its additive nonexception bits + (Table 108); the runtime's `run` returns the code and the status.""" d = s.data root = self.json_tree(d, "JsonParse", "JSON PARSE", d["into"]) text = self.field_expr(d["src"]) - self.json_code_store(s, "JSON PARSE", ind) - if "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases: - # IBM: malformed text, a type mismatch or a value that does not fit raises the exception condition (ON - # EXCEPTION runs, else NOT ON EXCEPTION). Items stored before a type or size failure keep their values. - v = self.tmpname("jsonfail") - out = [f"{ind}boolean {v} = __PACKAGE__.cobolrt.JsonParse.tryParse({text}, {root}, CS);"] - if "EXCEPTION" in s.phrases: - out += [f"{ind}if ({v}) {{", *self.block(s.phrases["EXCEPTION"], ind + " "), f"{ind}}}"] - if "NOT-EXCEPTION" in s.phrases: - out += [f"{ind}if (!{v}) {{", *self.block(s.phrases["NOT-EXCEPTION"], ind + " "), f"{ind}}}"] - return out - return [f"{ind}__PACKAGE__.cobolrt.JsonParse.parse({text}, {root}, CS);"] + code = "GG-JSON-CODE" in self.items + stat = False # JSON-STATUS needs its own register; a later slice (the runtime already computes bits 1 and 2) + phrases = "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases + if not (code or stat or phrases): + return [f"{ind}__PACKAGE__.cobolrt.JsonParse.parse({text}, {root}, CS);"] + # IBM: an exception stops the parse (ON EXCEPTION runs, else control goes to the end of the statement and + # NOT ON EXCEPTION is ignored); items stored before it keep their values (assumed, X77). + v = self.tmpname("jsoncode") + st = self.tmpname("jsonstat") + flags = f"{str(code).lower()}, {str(stat).lower()}" + out = [ + f"{ind}int[] {st} = new int[1];", + f"{ind}int {v} = __PACKAGE__.cobolrt.JsonParse.tryParse({text}, {root}, CS, {st}, {flags});", + ] + if code: + out.append(f"{ind}Cobol.store({self.field_expr(E.Ref('JSON-CODE'))}, BigDecimal.valueOf({v}), false, CS);") + if "EXCEPTION" in s.phrases: + out += [f"{ind}if ({v} != 0) {{", *self.block(s.phrases["EXCEPTION"], ind + " "), f"{ind}}}"] + if "NOT-EXCEPTION" in s.phrases: + out += [f"{ind}if ({v} == 0) {{", *self.block(s.phrases["NOT-EXCEPTION"], ind + " "), f"{ind}}}"] + return out def overflow(self, s: S.Stmt, call: str, ind: str) -> list[str]: """A STRING / UNSTRING call (true: OVERFLOW) and its ON OVERFLOW / NOT ON OVERFLOW phrases.""" diff --git a/tests/cobol_mainframe/test_det_json_generate.py b/tests/cobol_mainframe/test_det_json_generate.py index aca132e6e..3d95456b4 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -365,18 +365,16 @@ def test_json_code_is_zero_after_a_generate_and_only_declared_when_named(tmp_pat assert "JSON_CODE" not in plain.java.upper().replace("-", "_") -@pytest.mark.parametrize( - "body", - [ - ["JSON GENERATE OUT FROM REC ON EXCEPTION DISPLAY JSON-CODE", "END-JSON"], - ["JSON GENERATE OUT FROM REC", "DISPLAY JSON-CODE", "JSON PARSE OUT INTO REC"], - ], -) -def test_a_json_code_after_an_exception_is_refused_by_name(tmp_path, body): - r = _translate(tmp_path, *body) +def test_a_generate_json_code_after_an_exception_is_refused_by_name(tmp_path): + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC ON EXCEPTION DISPLAY JSON-CODE", "END-JSON") assert len(r.stats["holes"]) >= 1 +def test_a_json_code_beside_a_json_parse_is_translated(tmp_path): + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC", "DISPLAY JSON-CODE", "JSON PARSE OUT INTO REC") + assert r.stats["holes"] == [] + + def test_a_program_s_own_json_code_item_is_kept(tmp_path): src = tmp_path / "PROG.cbl" text = DATA.replace(" 01 N PIC 9(4).\n", " 01 N PIC 9(4).\n 01 JSON-CODE PIC 9(4).\n") diff --git a/tests/cobol_mainframe/test_det_json_parse.py b/tests/cobol_mainframe/test_det_json_parse.py index 03c27fcb9..76276b529 100644 --- a/tests/cobol_mainframe/test_det_json_parse.py +++ b/tests/cobol_mainframe/test_det_json_parse.py @@ -175,7 +175,8 @@ def test_the_runtime_stores_packed_and_binary_numbers(tmp_path): 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 assert out[0] == "12345D FFF4 007F", out[0] - assert out[1].startswith("EXC") and out[2].startswith("EXC") # 1000.00 does not fit S9(5)V99; 10000 not S9(4) + # 1000.00 does not fit S9(5)V99; 10000 not S9(4): IBM stores it anyway (JSON-STATUS 128), the stored form is unstated + assert out[1].startswith("HOLE") and "does not fit" in out[1] and out[2].startswith("HOLE") assert out[3].startswith("HOLE") and "unsigned" in out[3] assert out[4].startswith("HOLE") and "decimals" in out[4] @@ -223,7 +224,9 @@ def test_the_runtime_reads_ibms_text_and_stops_where_ibm_is_silent(tmp_path): System.out.println(name + " refused: " + e.getMessage()); } } - static void tryRun(String json, int digits, int scale, boolean signed) { + static void tryRun(String json, boolean wantCode, boolean wantStat) { + int digits = 3, scale = 0; + boolean signed = false; Storage st = new Storage(300); byte[] j = json.getBytes(CS); System.arraycopy(j, 0, st.bytes, 100, j.length); @@ -234,8 +237,9 @@ def test_the_runtime_reads_ibms_text_and_stops_where_ibm_is_silent(tmp_path): Field in = Field.alphanumeric(st, 100, 100, false); Field a = Field.alphanumeric(st, 0, 8, false); Field b = Field.zoned(st, 50, digits, scale, signed, false, false); - boolean exc = JsonParse.tryParse(in, JsonParse.group("G", JsonParse.text("A-B", a), JsonParse.number("N", b)), CS); - System.out.println((exc ? "EXC" : "OK") + "[" + new String(st.bytes, 0, 8, CS) + "][" + new String(st.bytes, 50, digits, CS) + "]"); + int[] stat = new int[1]; + int code = JsonParse.tryParse(in, JsonParse.group("G", JsonParse.text("A-B", a), JsonParse.number("N", b)), CS, stat, wantCode, wantStat); + System.out.println("C" + code + "S" + stat[0] + "[" + new String(st.bytes, 0, 8, CS) + "][" + new String(st.bytes, 50, digits, CS) + "]"); } catch (RuntimeException e) { System.out.println("HOLE " + e.getMessage()); } @@ -254,18 +258,26 @@ def test_the_runtime_reads_ibms_text_and_stops_where_ibm_is_silent(tmp_path): run("bad", "{\\"N\\":5", 3, 0, false); run("array", "{\\"N\\":[1]}", 3, 0, false); run("wide", "{\\"A-B\\":\\"\u00e9\\"}", 3, 0, false); - tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":7}", 3, 0, false); - tryRun("{\\"N\\":5", 3, 0, false); - tryRun("[1]", 3, 0, false); - tryRun("{\\"N\\":\\"5\\"}", 3, 0, false); - tryRun("{\\"A-B\\":\\"123456789\\"}", 3, 0, false); - tryRun("{\\"N\\":1000}", 3, 0, false); - tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":\\"x\\"}", 3, 0, false); - tryRun("{\\"N\\":1.5}", 3, 0, false); - tryRun("{\\"N\\":-1}", 3, 0, false); - tryRun("{\\"N\\":1e2}", 3, 0, false); - tryRun("{\\"N\\":1,\\"N\\":2}", 3, 0, false); - tryRun("{\\"A-B\\":\\"\u00e9\\"}", 3, 0, false); + tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":7}", true, false); + tryRun("{\\"N\\":5", true, false); + tryRun("[1]", true, false); + tryRun("{\\"N\\":\\"5\\"}", true, false); + tryRun("{\\"A-B\\":\\"123456789\\"}", true, false); + tryRun("{\\"N\\":1000}", true, false); + tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":\\"x\\"}", true, false); + tryRun("{\\"N\\":1.5}", true, false); + tryRun("{\\"N\\":-1}", true, false); + tryRun("{\\"N\\":1e2}", true, false); + tryRun("{\\"N\\":1,\\"N\\":2}", true, false); + tryRun(" ", true, false); + tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":7} x", true, false); + tryRun("{\\"other\\":1}", true, false); + tryRun("{\\"N\\":5}", true, true); + tryRun("{\\"A-B\\":\\"Ann\\",\\"N\\":7,\\"zz\\":1}", true, true); + tryRun("{\\"zz\\":1}", true, true); + tryRun("5", false, false); + tryRun("{\\"N\\":\\"5\\"}", true, true); + tryRun("{\\"A-B\\":\\"\u00e9\\"}", true, false); } } """ @@ -281,22 +293,27 @@ def test_the_runtime_reads_ibms_text_and_stops_where_ibm_is_silent(tmp_path): assert out[0] == "[Ann ][0001K]" assert out[1] == '[a"b ][007]' assert out[2] == "[########][005]" - assert out[3] == "[########][999]" - assert out[4] == "[########][999]" + assert "refused" in out[3] and "JSON-CODE 106" in out[3] # no name matched: IBM's exception 106 + assert "refused" in out[4] and "JSON-CODE 106" in out[4] # names are case-sensitive, so "n" matches nothing for i, why in ((5, "9 characters"), (6, "takes a JSON number"), (7, "decimals"), (8, "does not fit"), - (9, "unsigned"), (10, "exception condition"), (11, "array"), (12, "beyond ASCII")): # fmt: skip + (9, "unsigned"), (10, "JSON-CODE 100"), (11, "array"), (12, "beyond ASCII")): # fmt: skip assert "refused" in out[i] and why in out[i], out[i] # ON EXCEPTION: IBM's exception-class failures are caught; malformed text stores nothing, a type or size failure # keeps what was stored before it, and what IBM leaves undefined or the model does not take stays a Hole tail = out[13:] - assert tail[0] == "OK[Ann ][007]" - assert tail[1] == "EXC[########][999]" # unterminated object - assert tail[2] == "EXC[########][999]" # not an object - assert tail[3] == "EXC[########][999]" # string for a number - assert tail[4] == "EXC[########][999]" # nine characters for eight - assert tail[5] == "EXC[########][999]" # 1000 does not fit PIC 999 - assert tail[6] == "EXC[Ann ][999]" # A-B stored before N failed - for i, why in ((7, "decimals"), (8, "unsigned"), (9, "exponent"), (10, "occurs twice"), (11, "beyond ASCII")): + assert tail[0] == "C0S0[Ann ][007]" + assert tail[1] == "C100S0[########][999]" # unterminated object: invalid text + assert tail[3] == "C104S0[########][999]" # string for a number: incompatible value + assert tail[6] == "C104S0[Ann ][999]" # A-B stored before N failed + assert tail[11] == "C101S0[########][999]" # empty text + assert tail[12] == "C102S0[########][999]" # text after the closing brace: nothing stored (assumed) + assert tail[13] == "C106S0[########][999]" # no name matched + assert tail[14] == "C0S1[########][005]" # item A-B had no pair -> JSON-STATUS 1 + assert tail[15] == "C0S2[Ann ][007]" # the pair zz has no item -> JSON-STATUS 2 + assert tail[17] == "C-1S0[########][999]" # a bare number is not an object: the Programming Guide states no code + for i, why in ((2, "JSON array"), (4, "9 characters"), (5, "does not fit"), (7, "decimals"), + (8, "unsigned"), (9, "exponent"), (10, "occurs twice"), (16, "JSON-STATUS after an exception"), + (18, "JSON-STATUS after an exception"), (19, "beyond ASCII")): # fmt: skip assert tail[i].startswith("HOLE") and why in tail[i], tail[i] @@ -313,9 +330,9 @@ def test_on_exception_phrases_run_around_the_runtime_call(tmp_path): assert r.stats["holes"] == [] java = r.java assert "JsonParse.tryParse(" in java and "JsonParse.parse(" not in java - flag = next(x for x in java.splitlines() if "tryParse" in x).split()[1] - assert f"if ({flag}) {{" in java and f"if (!{flag}) {{" in java - assert java.index(f"if ({flag}) {{") < java.index(f"if (!{flag}) {{") + flag = next(x for x in java.splitlines() if "JsonParse.tryParse(" in x).split()[1] + assert f"if ({flag} != 0) {{" in java and f"if ({flag} == 0) {{" in java + assert java.index(f"if ({flag} != 0) {{") < java.index(f"if ({flag} == 0) {{") @pytest.mark.parametrize( @@ -330,10 +347,24 @@ def test_either_phrase_alone_and_an_unterminated_statement_bind_to_the_parse(tmp def test_without_a_phrase_the_call_stays_the_throwing_one(tmp_path): r = _translate(tmp_path, "JSON PARSE JIN INTO REC WITH DETAIL") - assert "JsonParse.parse(" in r.java and "tryParse" not in r.java + assert "JsonParse.parse(" in r.java and "JsonParse.tryParse(" not in r.java def test_a_shape_the_model_does_not_take_keeps_its_phrases_in_one_hole(tmp_path): r = _translate(tmp_path, "JSON PARSE JIN INTO REC CONVERTING ON EXCEPTION DISPLAY 'BAD'", "END-JSON") assert [h for h in r.stats["holes"] if "JSON PARSE" in str(h)] - assert "tryParse" not in r.java + assert "JsonParse.tryParse(" not in r.java + + +def test_json_code_and_status_are_stored_from_the_runtime_result(tmp_path): + r = _translate(tmp_path, "JSON PARSE JIN INTO REC", " ON EXCEPTION DISPLAY JSON-CODE", "END-JSON") + assert r.stats["holes"] == [] + java = r.java + assert "JsonParse.tryParse(" in java and "true, false);" in java + assert "GG_JSON_CODE" in java + + +def test_json_code_alone_asks_the_runtime_for_the_code_only(tmp_path): + r = _translate(tmp_path, "JSON PARSE JIN INTO REC", "DISPLAY JSON-CODE") + assert r.stats["holes"] == [] + assert "true, false);" in r.java and "GG_JSON_STATUS" not in r.java From 1cdad5f313217b59e94dc2b309ebf5d295e167ac Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 10:45:18 -0500 Subject: [PATCH 2/3] cobolrt API manifest: add JsonParse.tryParse status overload as public (Part of #7524, #7709) Co-Authored-By: Claude Sonnet 5.5 --- docs/language_status/cobolrt_api.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 0bb5ce166..b38548693 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": []}, @@ -1019,9 +1019,10 @@ {"id": "cobolrt.JsonParse", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java", "variants": ["det-batch", "det-standalone", "sort-job"], "supers": [], "visibility": "public", "evidence": ["emitter", "emitter-dynamic", "signature", "harness", "runtime"], "members": [ {"id": "cobolrt.JsonParse.group(String,Node...)", "kind": "method", "static": true, "returns": "cobolrt.JsonParse.Node", "visibility": "public", "evidence": ["emitter-dynamic", "harness"]}, {"id": "cobolrt.JsonParse.number(String,Field)", "kind": "method", "static": true, "returns": "cobolrt.JsonParse.Node", "visibility": "public", "evidence": ["harness"]}, - {"id": "cobolrt.JsonParse.parse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "void", "visibility": "public", "evidence": ["emitter", "harness", "runtime"]}, + {"id": "cobolrt.JsonParse.parse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "void", "visibility": "public", "evidence": ["emitter", "harness"]}, {"id": "cobolrt.JsonParse.text(String,Field)", "kind": "method", "static": true, "returns": "cobolrt.JsonParse.Node", "visibility": "public", "evidence": ["harness"]}, - {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "boolean", "visibility": "public", "evidence": ["emitter", "harness"]} + {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "boolean", "visibility": "public", "evidence": ["emitter", "harness"]}, + {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset,int[],boolean,boolean)", "kind": "method", "static": true, "returns": "int", "visibility": "public", "evidence": ["emitter", "harness", "runtime"]} ]}, {"id": "cobolrt.JsonParse.Node", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java", "variants": ["det-batch", "det-standalone", "sort-job"], "supers": [], "visibility": "public", "evidence": ["signature", "harness", "runtime"], "members": [ ]}, From 6214b19ce9f79df458fac260cbda284ac2926050 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 6 Nov 2026 12:51:49 -0500 Subject: [PATCH 3/3] cobolrt API manifest: keep JsonParse.tryParse status overload public after regenerate (Part of #7524, #7709) 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 eb6c51baf..b09c33b8c 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": []}, @@ -1022,7 +1022,7 @@ {"id": "cobolrt.JsonParse.parse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "void", "visibility": "public", "evidence": ["emitter", "harness"]}, {"id": "cobolrt.JsonParse.text(String,Field)", "kind": "method", "static": true, "returns": "cobolrt.JsonParse.Node", "visibility": "public", "evidence": ["harness"]}, {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset)", "kind": "method", "static": true, "returns": "boolean", "visibility": "public", "evidence": ["emitter", "harness"]}, - {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset,int[],boolean,boolean)", "kind": "method", "static": true, "returns": "int", "visibility": "internal", "evidence": ["emitter", "harness", "runtime"]} + {"id": "cobolrt.JsonParse.tryParse(Field,Node,Charset,int[],boolean,boolean)", "kind": "method", "static": true, "returns": "int", "visibility": "public", "evidence": ["emitter", "harness", "runtime"]} ]}, {"id": "cobolrt.JsonParse.Node", "kind": "class", "source": "gitgalaxy/tools/cobol_to_java/det/cobolrt/JsonParse.java", "variants": ["det-batch", "det-standalone", "sort-job"], "supers": [], "visibility": "public", "evidence": ["signature", "harness", "runtime"], "members": [ ]},