diff --git a/docs/language_status/cobolrt_rules.json b/docs/language_status/cobolrt_rules.json index b38f762ebf..fd307722bc 100644 --- a/docs/language_status/cobolrt_rules.json +++ b/docs/language_status/cobolrt_rules.json @@ -1982,10 +1982,12 @@ { "id": "RT-FUNC-DATE-INTEGER", "family": "FUNC", - "title": "FUNCTION INTEGER-OF-DATE / DATE-OF-INTEGER: day 1 is 1 January 1601 (INTDATE(ANSI))", + "title": "FUNCTION INTEGER-OF-DATE / DATE-OF-INTEGER: day 1 is 1 January 1601 (INTDATE(ANSI)) or 15 October 1582 (INTDATE(LILIAN))", "methods": [ "integerOfDate", - "dateOfInteger" + "dateOfInteger", + "integerOfDateLilian", + "dateOfIntegerLilian" ], "ibm": { "doc": "cobol-lr", diff --git a/docs/language_status/register/C5.md b/docs/language_status/register/C5.md index 98e2616446..366fc55959 100644 --- a/docs/language_status/register/C5.md +++ b/docs/language_status/register/C5.md @@ -1,8 +1,8 @@ --- id: "C5" family: "C" -status: "MATCHED (NUMPROC) / MODELLED (TRUNC(OPT) and ARITH(EXTEND), det ports; a stop by name) / REFUSED (the rest)" -title: "Compiler options: NUMPROC MATCHED where IBM pins it (#4271); TRUNC(OPT) and ARITH(EXTEND) MODELLED (#4706); INTDATE(LILIAN) REFUSED" +status: "MATCHED (NUMPROC) / MODELLED (TRUNC(OPT) and ARITH(EXTEND), det ports, a stop by name; INTDATE(LILIAN), det port, no oracle) / REFUSED (the rest)" +title: "Compiler options: NUMPROC MATCHED where IBM pins it (#4271); TRUNC(OPT) and ARITH(EXTEND) MODELLED (#4706); INTDATE(LILIAN) MODELLED in the det port, refused by the oracle" area: "compiler" summary: "`NUMPROC(MIG)` as Enterprise COBOL 5+ compiles it (NOPFD), `NUMPROC(PFD)` with preferred signs (#4271); `TRUNC(OPT)` with conforming binary values, `ARITH(EXTEND)` with intermediates within 30 digits (#4706); `INTDATE(LILIAN)`" reached: "NUMPROC: no; TRUNC(OPT): CBSA (det ports); ARITH(EXTEND): DBB EPSCSMRD (det ports)" @@ -51,12 +51,15 @@ reached: "NUMPROC: no; TRUNC(OPT): CBSA (det ports); ARITH(EXTEND): DBB EPSCSMRD COMPAT and EXTEND agree or is not judged. A floating-point expression under EXTEND is still refused at translation (`Untranslatable`). A port without the stop (model, generated) is proven as COMPAT, a declared difference in its evidence record (`option_differences`). Among the burned estates only DBB's EPSCSMRD carries `PROCESS ARITH(EXTEND)`. -- **REFUSED:** `INTDATE(LILIAN)`, from a CBL/PROCESS card or a case's `compiler_options`, stops the run - (`UnsupportedOption`). - - `INTDATE(LILIAN)` is pinned by IBM (day 1 is 15 October 1582 instead of 1 January 1601), and could be modelled - by the documented offset of the integer-date functions. Dates before 1601 cannot run on GnuCOBOL and would be - refused. No case needs it yet: no burned estate's program carries `INTDATE(LILIAN)` on a CBL/PROCESS card - (#4271 status check), so it stays refused by name rather than modelled. +- **INTDATE(LILIAN): MODELLED in the det port (#4706), REFUSED by the oracle.** IBM pins it (day 1 is 15 October 1582 + instead of 1 January 1601: the same date is 6653 higher). The translator reads the option through the one resolver + (`program.intdate_lilian`: CBL / PROCESS cards over the compile step's PARM over the default, ANSI) and emits + `Funcs.integerOfDateLilian` / `dateOfIntegerLilian` for INTEGER-OF-DATE / DATE-OF-INTEGER (rule + `RT-FUNC-DATE-INTEGER`); it is fixed at translation, with no run-time switch. GnuCOBOL has no INTDATE, so + `COBC_OPTIONS` still refuses it (`UnsupportedOption`): a LILIAN program is not compared with cobc, and the port is + checked against IBM's documented day numbers (`test_intdate_lilian.py`), residual risk stated. Dates before day 1 of + the count are not defined by IBM and are not judged. No burned estate's program carries `INTDATE(LILIAN)` (#4271 + status check, re-confirmed by #4706's slice 2), so no estate port changed. ### Compile options: the table (#4706) One row per option that can change a result: (1) does it change results; (2) can cobc honour it; (3) what the det @@ -69,7 +72,7 @@ instrument). The rows below are rendered by one table in code (`compiler_options | `TRUNC(BIN)` | yes: a binary receiver keeps its bytes | `-fnotrunc` | `Cobol.swapTruncBinary(false)` | -- | C1 | | `TRUNC(OPT)` | only for a binary value past its PICTURE, where IBM's result is unpredictable; conforming values compute as STD | `-fbinary-truncate` (STD: equal for conforming values) | STD, plus `Cobol.swapTruncOpt(true)`: a value past a COMP / COMP-4 / BINARY receiver's PICTURE stops by name ("TRUNC(OPT): value exceeds PICTURE; IBM result unpredictable") | a port without the stop (model, generated) is proven as STD: a declared difference (`option_differences`) | C1, C5 | | `ARITH(COMPAT)` / `ARITH(EXTEND)` | yes: EXTEND widens intermediates to 31 digits and floats to extended precision | COMPAT flags (no cap on either side, C2) | COMPAT, plus `Cobol.swapArithExtend(true)`: an intermediate past 30 digits stops by name ("ARITH(EXTEND): an intermediate result of N digits"); a float is refused | a port without the stop is proven as COMPAT: a declared difference (`option_differences`) | C2, C5 | -| `INTDATE(ANSI)` / `INTDATE(LILIAN)` | yes: day 1 of INTEGER-OF-DATE moves from 1601-01-01 to 1582-10-15 | none for LILIAN | none | `INTDATE(LILIAN)` refused by name (`COBC_OPTIONS`) | C5 | +| `INTDATE(ANSI)` / `INTDATE(LILIAN)` | yes: day 1 of INTEGER-OF-DATE moves from 1601-01-01 to 1582-10-15 | none for LILIAN | `Funcs.integerOfDateLilian` / `dateOfIntegerLilian`, picked at translation | the oracle refuses `INTDATE(LILIAN)` by name (`COBC_OPTIONS`); the port is checked against IBM's day numbers, not cobc | C5 | | `NUMPROC(NOPFD\|PFD\|MIG)` | only for a sign that is not preferred | none needed | PFD refuses a non-preferred sign (`Codec.numprocPfd`) | MIG compiles as NOPFD from Enterprise COBOL 5 (`numproc_mig`) | C5 | | `ZONEDATA(PFD)` (IBM default) | no for valid zone bits (an invalid one is refused by C11) | none needed | refuses a non-digit zoned operand | -- | C11 | | `ZONEDATA(NOPFD\|MIG)` | yes: defines what invalid zone bits / digits do in MOVE, compare and arithmetic (APAR PI57769) | none | none | refused by name | C5 | diff --git a/gitgalaxy/tools/cobol_to_java/det/cobolrt/Funcs.java b/gitgalaxy/tools/cobol_to_java/det/cobolrt/Funcs.java index d9536b7654..6ce8dd5545 100644 --- a/gitgalaxy/tools/cobol_to_java/det/cobolrt/Funcs.java +++ b/gitgalaxy/tools/cobol_to_java/det/cobolrt/Funcs.java @@ -210,6 +210,23 @@ public static BigDecimal dateOfInteger(BigDecimal days) { return BigDecimal.valueOf(d.getYear() * 10000L + d.getMonthValue() * 100L + d.getDayOfMonth()); } + private static final java.time.LocalDate LILIAN_DAY_ZERO = java.time.LocalDate.of(1582, 10, 14); + + /** INTEGER-OF-DATE under INTDATE(LILIAN) (#4706): day 1 is 15 October 1582, so the same date is 6653 higher than + * under ANSI. */ + // [rule RT-FUNC-DATE-INTEGER] + public static BigDecimal integerOfDateLilian(BigDecimal yyyymmdd) { + int v = yyyymmdd.intValue(); + java.time.LocalDate d = java.time.LocalDate.of(v / 10000, v / 100 % 100, v % 100); + return BigDecimal.valueOf(java.time.temporal.ChronoUnit.DAYS.between(LILIAN_DAY_ZERO, d)); + } + + /** DATE-OF-INTEGER under INTDATE(LILIAN) (#4706): days since 14 October 1582 -> YYYYMMDD. */ + public static BigDecimal dateOfIntegerLilian(BigDecimal days) { + java.time.LocalDate d = LILIAN_DAY_ZERO.plusDays(days.longValue()); + return BigDecimal.valueOf(d.getYear() * 10000L + d.getMonthValue() * 100L + d.getDayOfMonth()); + } + /** FUNCTION TEST-NUMVAL: 0 when the argument is valid for NUMVAL, else the (1-based) position of the first * character that makes it invalid -- the argument's length + 1 when it ends before a digit (GnuCOBOL, * checked: one sign, leading or trailing; + - CR DB in upper case; nothing but spaces after a trailing one). */ diff --git a/gitgalaxy/tools/cobol_to_java/det/program.py b/gitgalaxy/tools/cobol_to_java/det/program.py index 333e39577d..1af1a1b13f 100644 --- a/gitgalaxy/tools/cobol_to_java/det/program.py +++ b/gitgalaxy/tools/cobol_to_java/det/program.py @@ -402,6 +402,24 @@ def arith_extend(program: Path, options: list[str] | None = None, declared: str return str(eff.get("ARITH") or "").upper() == "EXTEND" +def intdate_lilian(program: Path, options: list[str] | None = None, declared: str | None = None) -> bool: + """Whether the program is compiled with INTDATE(LILIAN) (#4706): its CBL / PROCESS cards over `options` (the compile + step's PARM), else IBM's default, ANSI -- the same resolver as TRUNC and ARITH (#4704).""" + from gitgalaxy.core.compiler_options import effective_with_defaults + from gitgalaxy.core.source_text import read_source + + eff = effective_with_defaults(options, read_source(program, declared=declared).text) + return str(eff.get("INTDATE") or "").upper() == "LILIAN" + + +def with_intdate_lilian(java: str) -> str: + """INTDATE(LILIAN) (#4706): the integer-date functions count from 1582-10-15, `Funcs.*Lilian`. Fixed at translation + (no run-time switch): the option is the program's own, so a LINK or CALL into another program leaves it alone.""" + return java.replace("Funcs.integerOfDate(", "Funcs.integerOfDateLilian(").replace( + "Funcs.dateOfInteger(", "Funcs.dateOfIntegerLilian(" + ) + + def numproc_pfd(program: Path, options: list[str] | None = None, declared: str | None = None) -> bool: """Whether the program runs under NUMPROC(PFD) (#4271): the NUMPROC option in effect -- its CBL / PROCESS cards over `options` (the compile step's PARM), else IBM's default, NOPFD. NUMPROC(MIG) is NOPFD: Enterprise COBOL 5 and 6 no @@ -1317,6 +1335,8 @@ def callee_types(cls: str) -> list[str]: numproc_pfd(program, options, page), arith_extend(program, options, page), ) + if intdate_lilian(program, options, page): + java = with_intdate_lilian(java) return Result(java, service, stats) diff --git a/tests/cobol_mainframe/test_intdate_lilian.py b/tests/cobol_mainframe/test_intdate_lilian.py new file mode 100644 index 0000000000..661762a1f7 --- /dev/null +++ b/tests/cobol_mainframe/test_intdate_lilian.py @@ -0,0 +1,119 @@ +"""#4706: INTDATE(LILIAN) in the det port (oracle_assumptions.md C5). + +IBM is the reference. INTDATE(LILIAN) moves day 1 of FUNCTION INTEGER-OF-DATE / DATE-OF-INTEGER from 1 January 1601 +(ANSI, the default) to 15 October 1582, so a date is 6653 higher (Enterprise COBOL for z/OS 6.x Language Reference, +"INTEGER-OF-DATE"; Programming Guide, "INTDATE"). GnuCOBOL has no INTDATE, so the oracle still refuses the option by +name and a LILIAN program is not compared with cobc: the det port is checked here against IBM's documented day +numbers. The translator reads the option through the one resolver (CBL / PROCESS cards over the PARM over the default) +and picks `Funcs.*Lilian` at translation. + +The runtime tests need a JDK 17 (JAVA_HOME / JDK_17).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "tests" / "tools")) +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +# the epochs, a century year that is a leap year (1600) and one that is not (1700), a modern leap day, today, and the +# 1601 boundary where the two counts differ by exactly 6653 +DATES = [15821015, 16000229, 16010101, 17000228, 20000229, 20261018] + + +def _expected(): + import datetime + + out = [] + for ymd in DATES: + d = datetime.date(ymd // 10000, ymd // 100 % 100, ymd % 100) + ansi = (d - datetime.date(1600, 12, 31)).days + lilian = (d - datetime.date(1582, 10, 14)).days + out.append((ymd, ansi, lilian)) + return out + + +def test_the_documented_anchors(): + rows = {ymd: (a, lil) for ymd, a, lil in _expected()} + assert rows[15821015][1] == 1 # day 1 of LILIAN is 15 October 1582 + assert rows[16010101] == (1, 6654) # day 1 of ANSI + assert all(lil - a == 6653 for ymd, (a, lil) in rows.items() if ymd >= 16010101) + + +def _service(java: str) -> str: + pytest.importorskip("tree_sitter_language_pack") + from gitgalaxy.tools.cobol_to_java.det import program as P + + return P.with_intdate_lilian(java) + + +def test_the_translator_swaps_both_functions_and_only_them(): + java = "a = Funcs.integerOfDate(x); b = Funcs.dateOfInteger(y); c = Funcs.integerPart(z);\n" + out = _service(java) + assert out == "a = Funcs.integerOfDateLilian(x); b = Funcs.dateOfIntegerLilian(y); c = Funcs.integerPart(z);\n" + assert _service(out) == out # idempotent: a swapped call is not swapped again + + +@pytest.mark.parametrize( + ("options", "card", "lilian"), + [ + ([], "", False), # IBM's default + (["INTDATE(LILIAN)"], "", True), # the compile step's PARM + ([], " CBL INTDATE(LILIAN)", True), # the program's own card + (["INTDATE(LILIAN)"], " CBL INTDATE(ANSI)", False), # the card wins over the PARM + ([], " PROCESS INTDATE(LILIAN),TRUNC(BIN)", True), + ], +) +def test_the_option_comes_through_the_one_resolver(options, card, lilian, tmp_path): + pytest.importorskip("tree_sitter_language_pack") + from gitgalaxy.tools.cobol_to_java.det import program as P + + src = tmp_path / "p.cbl" + src.write_text((card + "\n" if card else "") + " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n") + assert P.intdate_lilian(src, options) is lilian + + +def _jdk_or_skip(): + pytest.importorskip("tree_sitter_language_pack") + import test_det_programs as T + + if T._java() is None: + pytest.skip("needs a JDK 17 (JAVA_HOME / JDK_17)") + return T + + +def _program(name: str, dates: list[int], card: str) -> str: + proc = [] + for ymd in dates: + proc += [ + f"COMPUTE N = FUNCTION INTEGER-OF-DATE({ymd})", + f"DISPLAY 'I {ymd} ' N", + "COMPUTE D = FUNCTION DATE-OF-INTEGER(N)", + f"DISPLAY 'D {ymd} ' D", + ] + src = [*([card] if card else []), " IDENTIFICATION DIVISION.", f" PROGRAM-ID. {name}.", + " DATA DIVISION.", " WORKING-STORAGE SECTION.", " 01 N PIC 9(8).", " 01 D PIC 9(8).", + " PROCEDURE DIVISION.", *(f" {x}" for x in proc), " GOBACK."] # fmt: skip + assert all(len(x) <= 72 for x in src) + return "\n".join(src) + "\n" + + +@pytest.mark.parametrize("typed", [False, True]) +def test_the_port_counts_from_the_option_in_effect(typed, tmp_path): + T = _jdk_or_skip() + rows = _expected() + dates = DATES + for sub, card, col in (("lil", " CBL INTDATE(LILIAN)", 2), ("ans", "", 1)): + (tmp_path / sub).mkdir() + out = T._java_run("intd", _program("INTD", dates, card), tmp_path / sub, typed) + for row in rows: + ymd, day = row[0], row[col] + if day < 1: # before day 1 of the count: IBM defines no integer for it (ANSI: before 1601) + continue + assert f"I {ymd} {day:08d}" in out, (sub, ymd, out) + assert f"D {ymd} {ymd:08d}" in out, (sub, ymd, out) # DATE-OF-INTEGER inverts it under the same option