diff --git a/docs/language_status/cobolrt_api.json b/docs/language_status/cobolrt_api.json index 22402c087..6aa1f6574 100644 --- a/docs/language_status/cobolrt_api.json +++ b/docs/language_status/cobolrt_api.json @@ -1020,9 +1020,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": [ ]}, diff --git a/docs/language_status/register/X76.md b/docs/language_status/register/X76.md index 89b34c636..801c21223 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] [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-STATUS, 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 JSON PARSE tables are later slices of #7524. +- **What we assume.** (1) The elementary-source wrapper 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 and without JSON-CODE named the exception is a runtime `Hole`; a program that names JSON-CODE takes the exception branch too (JSON-CODE 1, below). (5) A fixed table or an OCCURS DEPENDING ON table (below) 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-STATUS, 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. +- **JSON-CODE (#7524, #7709).** A program that names JSON-CODE gets a hidden item of IBM's declared shape, S9(9) BINARY, set to 0 after a JSON GENERATE that raised no exception (the LR states this) and to 1 after the exception "receiver too small" (Programming Guide, Table 107 as #7928 read it from IBM's PDF; not re-read here and not backed by an oracle). With ON EXCEPTION / NOT ON EXCEPTION the phrases run on that condition; without them control goes to the end of the statement. A program's own JSON-CODE item is kept. JSON PARSE sets the register from Table 109 (X77). JSON-STATUS is not modelled. - **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/docs/language_status/register/X77.md b/docs/language_status/register/X77.md index 5fb3beaa8..cab9379d5 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 is S9(9) BINARY, IBM's declared shape (#7709). (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 5523248db..f2cbf59f2 100644 --- a/gitgalaxy/tools/cobol_to_java/det/gen.py +++ b/gitgalaxy/tools/cobol_to_java/det/gen.py @@ -3364,53 +3364,57 @@ 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) - 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. + code = "GG-JSON-CODE" in self.items + if code or "EXCEPTION" in s.phrases or "NOT-EXCEPTION" in s.phrases: + # IBM: a receiver too small raises the exception condition (JSON-CODE 1, Programming Guide Table 107; ON + # EXCEPTION runs, else NOT ON EXCEPTION, and without the phrases control goes to the end of the statement). + # The receiver and COUNT are undefined then; the port leaves them as they were. v = self.tmpname("jsonbytes") out = [f"{ind}int {v} = __PACKAGE__.cobolrt.JsonGenerate.tryGenerate({into}, {root}, CS);"] if d["count"] is not None: n = self.field_expr(d["count"]) out.append(f"{ind}if ({v} >= 0) Cobol.store({n}, BigDecimal.valueOf({v}), false, CS);") + if code: + jc = self.field_expr(E.Ref("JSON-CODE")) + out.append(f"{ind}Cobol.store({jc}, BigDecimal.valueOf({v} < 0 ? 1 : 0), 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 if d["count"] is None: - return [f"{ind}{call};", *reg] + return [f"{ind}{call};"] 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): - 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") - return [f"{ind}Cobol.store({self.field_expr(E.Ref('JSON-CODE'))}, BigDecimal.ZERO, false, CS);"] + return [f"{ind}Cobol.store({n}, BigDecimal.valueOf({call}), 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/gitgalaxy/tools/cobol_to_java/det/program.py b/gitgalaxy/tools/cobol_to_java/det/program.py index caeba753a..de6db1a7d 100644 --- a/gitgalaxy/tools/cobol_to_java/det/program.py +++ b/gitgalaxy/tools/cobol_to_java/det/program.py @@ -1006,7 +1006,7 @@ def _translate(program: Path, copy_dirs: list[Path], stub: str, package: str, es srt = L.Item(1, "GG-SORT-RETURN", "WORKING-STORAGE", pic="S9(4)", usage="BINARY") L.layout(srt) records.append(srt) - # #7524: JSON-CODE, the special register a JSON GENERATE sets (0 after a success, IBM LR), S9(4) BINARY -- only in a + # #7524: JSON-CODE, the special register a JSON statement sets (0 after a success, IBM LR), S9(9) BINARY -- only in a # program that names it and has a JSON statement, so no other port's storage changes json_stmts = any(s.kind in ("JSONGEN", "JSONPARSE") for p in proc.paragraphs for s in S.walk(p.body)) if ( @@ -1014,7 +1014,7 @@ def _translate(program: Path, copy_dirs: list[Path], stub: str, package: str, es and any(re.search(r"\bJSON-CODE\b", ln.text, re.I) for ln in lines) and not any(it.name == "JSON-CODE" for r in records for it in r.walk()) ): - jc = L.Item(1, "GG-JSON-CODE", "WORKING-STORAGE", pic="S9(4)", usage="BINARY") + jc = L.Item(1, "GG-JSON-CODE", "WORKING-STORAGE", pic="S9(9)", usage="BINARY") L.layout(jc) records.append(jc) svc_m = re.search(r"public class (\w+)", stub) diff --git a/tests/cobol_mainframe/test_det_json_generate.py b/tests/cobol_mainframe/test_det_json_generate.py index 997f375c2..09edb7f83 100644 --- a/tests/cobol_mainframe/test_det_json_generate.py +++ b/tests/cobol_mainframe/test_det_json_generate.py @@ -406,7 +406,7 @@ def test_json_code_is_zero_after_a_generate_and_only_declared_when_named(tmp_pat ) assert not r.stats["holes"] assert "GG_JSON_CODE" in r.java.upper().replace("-", "_") - assert r.java.count("BigDecimal.ZERO") >= 1 + assert "< 0 ? 1 : 0" in r.java # 1 for an exception (receiver too small), 0 otherwise plain = _translate(tmp_path, "JSON GENERATE OUT FROM REC") assert "JSON_CODE" not in plain.java.upper().replace("-", "_") @@ -414,13 +414,32 @@ def test_json_code_is_zero_after_a_generate_and_only_declared_when_named(tmp_pat @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"], + ("JSON GENERATE OUT FROM REC ON EXCEPTION DISPLAY JSON-CODE", "END-JSON"), + ("JSON GENERATE OUT FROM REC NOT ON EXCEPTION DISPLAY JSON-CODE", "END-JSON"), + ( + "JSON GENERATE OUT FROM REC COUNT N ON EXCEPTION DISPLAY 'X'", + "NOT ON EXCEPTION DISPLAY JSON-CODE", + "END-JSON", + ), + ("JSON GENERATE OUT FROM REC", "DISPLAY JSON-CODE"), ], ) -def test_a_json_code_after_an_exception_is_refused_by_name(tmp_path, body): +def test_a_generate_json_code_is_one_after_an_exception_and_zero_otherwise(tmp_path, body): + """#7524 (X76): IBM Programming Guide Table 107, code 1 = receiver too small; every phrase / COUNT shape sets it.""" r = _translate(tmp_path, *body) - assert len(r.stats["holes"]) >= 1 + assert r.stats["holes"] == [] + assert "tryGenerate(" in r.java and "< 0 ? 1 : 0" in r.java + + +def test_the_json_code_register_is_ibms_nine_digit_binary(tmp_path): + """IBM's JSON-CODE is S9(9) USAGE BINARY: a code above S9(4) must not overflow the hidden register.""" + r = _translate(tmp_path, "JSON GENERATE OUT FROM REC", "DISPLAY JSON-CODE") + assert "f10_GG_JSON_CODE = Field.binary(s_GG_JSON_CODE, 0, 9, 0, true, false)" in r.java + + +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): 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