Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions gitgalaxy/core/mainframe_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,16 +1199,28 @@ def _context(offset: int) -> tuple[Optional[str], Optional[str]]:
usage_match = _USAGE_CLAUSE.search(entry)
usage = usage_match.group(1).upper() if usage_match else None

# #3816: PIC G is always DISPLAY-1 (DBCS); PIC N is NATIONAL unless NSYMBOL(DBCS); a group's
# USAGE NATIONAL / DISPLAY-1 applies to its members. (Other group usages are not inherited here.)
# #3816: PIC G is always DISPLAY-1 (DBCS); PIC N is NATIONAL unless NSYMBOL(DBCS).
if usage is None and pic and _NATIONAL_PICTURE.fullmatch(pic.upper()):
# (a real national picture only: GnuCOBOL's malformed `PIC USAGE BINARY-SHORT` has a G in it)
usage = "DISPLAY-1" if "G" in pic.upper() or is_dbcs else "NATIONAL"
if usage is None and parent_usage in ("NATIONAL", "DISPLAY-1"):
# #4525: a group's USAGE applies to every item under it that has none of its own (IBM Enterprise
# COBOL, COBOL 2002): `01 COMP-VARIABLES COMP.` + `05 CR-CNT PIC S9(4).` is a 2-byte binary, not
# 4 zoned bytes. The nearest enclosing group's (effective) usage wins -- `parent_usage` is already
# inherited itself -- so a nested group's own USAGE overrides an outer one for its members. A
# 66 / 88 entry (no storage) keeps the #3816 shape: only a NATIONAL / DISPLAY-1 usage rides on it.
# An inherited DISPLAY is the default anyway: it is not recorded, but it still stops an outer
# group's usage (`01 G COMP.` + `05 S DISPLAY.` + `10 X PIC 9.` is one zoned byte).
in_force = usage or parent_usage
if (
usage is None
and parent_usage
and parent_usage.upper() != "DISPLAY"
and (level not in _CONDITION_LEVELS or parent_usage in ("NATIONAL", "DISPLAY-1"))
):
usage = parent_usage

if level not in _CONDITION_LEVELS:
stack.append((level, ordinal, usage))
stack.append((level, ordinal, in_force))
last_item_ordinal = ordinal

occurs_match = _OCCURS_CLAUSE.search(window)
Expand Down
15 changes: 13 additions & 2 deletions gitgalaxy/tools/cobol_to_cobol/cobol_schema_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def data_entries(content: str) -> list[dict]:

code = _blank_literals("\n".join(_code_lines(content)))
entries = []
groups: list[tuple[int, str | None]] = [] # #4525: (level, effective usage) of the open groups
for raw in re.split(r"\.(?=\s|$)", code):
m = _LEVEL_ENTRY.match(raw)
if not m:
Expand All @@ -120,13 +121,23 @@ def data_entries(content: str) -> list[dict]:
if name in _CLAUSE_WORDS: # an unnamed item: the "name" is its first clause
rest, name = f"{name} {rest}", None
pic = _PIC_CLAUSE.search(rest)
usage = _USAGE_CLAUSE.search(rest)
usage_match = _USAGE_CLAUSE.search(rest)
usage = usage_match.group(1) if usage_match else None
# #4525: a group's USAGE applies to every item under it without one of its own (as the engine)
lvl = int(level)
if lvl not in (66, 88):
while groups and (groups[-1][0] >= lvl or lvl == 77):
groups.pop()
effective = usage or (groups[-1][1] if groups else None)
groups.append((lvl, effective))
if effective and effective.upper() != "DISPLAY": # an inherited DISPLAY is the default anyway
usage = effective
entries.append(
{
"level": level,
"name": name,
"pic": pic.group(1) if pic else None,
"usage": usage.group(1) if usage else None,
"usage": usage,
"depending": "DEPENDING ON" in re.sub(r"\s+", " ", rest),
}
)
Expand Down
80 changes: 58 additions & 22 deletions gitgalaxy/tools/cobol_to_cobol/galaxy_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -2178,17 +2178,25 @@ def record_layout(self, ef: EngineFile, item: EngineDataItem, extension: Optiona
state = {"variable": False, "unknown": False}
extension_files = sorted({f.file_path for f, _ in extension or []})

def _walk(owner: EngineFile, it: EngineDataItem, offset: int, depth: int, sink: Optional[list] = None) -> int:
def _walk(
owner: EngineFile,
it: EngineDataItem,
offset: int,
depth: int,
sink: list | None = None,
inherited: str | None = None,
) -> int:
"""`it`'s width; its elementary fields go to `sink` (`fields`, or a throwaway list
when only an overlay's width is wanted)."""
when only an overlay's width is wanted). `inherited` is the enclosing group's USAGE (#4525)."""
out = fields if sink is None else sink
if it.level in (66, 88):
return 0
if it.occurs_depending_on:
state["variable"] = True
times = it.occurs_max or 1
usage = _usage_under(it, inherited)
# An elementary item's only children are its 88/66 conditions.
kids = [] if _is_elementary(it) else self._expanded_children(owner, it, ef, depth)
kids = [] if _is_elementary(it, usage) else self._expanded_children(owner, it, ef, depth)
if it is item and extension:
kids = kids + list(extension)
if kids:
Expand All @@ -2210,16 +2218,16 @@ def _walk(owner: EngineFile, it: EngineDataItem, offset: int, depth: int, sink:
kid_depth = depth + (kid_file is not owner)
if kid.redefines:
if region is not None and region[2] == kid.redefines.upper():
wide = _walk(kid_file, kid, region[0], kid_depth, [])
wide = _walk(kid_file, kid, region[0], kid_depth, [], usage)
if wide > region[1]:
size += wide - region[1]
region[1] = wide
continue
kid_width = _walk(kid_file, kid, offset + size, kid_depth, sink)
kid_width = _walk(kid_file, kid, offset + size, kid_depth, sink, usage)
region = [offset + size, kid_width, kid.name.upper()]
size += kid_width
return size * times
width = _elementary_bytes(it)
width = _elementary_bytes(it, usage)
if width is None:
state["unknown"] = True
width = 0
Expand All @@ -2228,8 +2236,8 @@ def _walk(owner: EngineFile, it: EngineDataItem, offset: int, depth: int, sink:
"name": it.name,
"level": it.level,
"pic": it.pic,
"usage": it.usage,
"class": _item_class(it),
"usage": usage,
"class": _item_class(it, usage),
"offset": offset,
"bytes": width * times,
"occurs": it.occurs_max,
Expand Down Expand Up @@ -4868,12 +4876,20 @@ def _storage_spans(self, ef: EngineFile) -> dict:
items: dict = {} # record key -> [(offset, bytes, depth, name)]

def walk(
owner: EngineFile, it: EngineDataItem, key: tuple, offset: int, depth: int, ext: Optional[list], path: tuple
owner: EngineFile,
it: EngineDataItem,
key: tuple,
offset: int,
depth: int,
ext: list | None,
path: tuple,
inherited: str | None = None,
):
if it.level in (66, 88) or depth > 12:
return 0
times = it.occurs_max or 1
kids = [] if _is_elementary(it) else self._expanded_children(owner, it, ef, depth)
usage = _usage_under(it, inherited) # #4525: a group's USAGE reaches its COPYed members too
kids = [] if _is_elementary(it, usage) else self._expanded_children(owner, it, ef, depth)
if ext:
kids = kids + list(ext)
if kids:
Expand All @@ -4889,7 +4905,7 @@ def walk(
if kid.redefines:
base = at.get(kid.redefines.upper())
at_base = base if base is not None else offset + (size or 0)
wide = walk(kid_file, kid, key, at_base, depth + 1, None, (it.name, *path))
wide = walk(kid_file, kid, key, at_base, depth + 1, None, (it.name, *path), usage)
if (
region is not None
and region[0] == kid.redefines.upper()
Expand All @@ -4901,12 +4917,12 @@ def walk(
region[1] = wide
continue
at[kid.name.upper()] = offset + (size or 0)
width = walk(kid_file, kid, key, offset + (size or 0), depth + 1, None, (it.name, *path))
width = walk(kid_file, kid, key, offset + (size or 0), depth + 1, None, (it.name, *path), usage)
region = [kid.name.upper(), width]
size = None if size is None or width is None else size + width
total = None if size is None else size * times
else:
width = _elementary_bytes(it)
width = _elementary_bytes(it, usage)
total = None if width is None else width * times
# One occurrence's width rides along: a subscripted reference moves one.
spans[id(it)] = (key, offset, total, None if total is None else total // times)
Expand Down Expand Up @@ -5557,16 +5573,35 @@ def _last_entry(item: EngineDataItem) -> EngineDataItem:
return item


def _is_elementary(item: EngineDataItem) -> bool:
return bool(item.pic) or (item.usage or "").upper() in _PICLESS_USAGES
def _is_elementary(item: EngineDataItem, inherited: str | None = None) -> bool:
"""Whether `item` is an elementary item: it has a PIC, or a usage that needs none (COMP-1/2,
POINTER, INDEX) and no subordinate items. #4525: the usage may be a group's -- `inherited`, or
stored on a nested group by the extractor -- so a usage alone does not make an item elementary:
`01 PTRS POINTER.` + `05 P1.` + `05 P2.` is a group of two pointers."""
if item.pic:
return True
if (item.usage or inherited or "").upper() not in _PICLESS_USAGES:
return False
return not any(c.level not in (66, 88) for c in item.children)


def _usage_under(item: EngineDataItem, inherited: str | None) -> str | None:
"""#4525: `item`'s effective USAGE -- its own, else the nearest enclosing group's (`inherited`,
IBM Enterprise COBOL: a group's USAGE applies to every elementary item under it without one).
The extractor already resolves this within a source; a COPY member's items only learn the
usage of the group they are COPYed under here. PL/I attributes are not inherited this way."""
if item.usage or item.attributes is not None or item.level in (66, 88):
return item.usage
return inherited


def _item_class(item: EngineDataItem) -> str:
def _item_class(item: EngineDataItem, inherited: str | None = None) -> str:
"""A coarse storage class for shape comparison: X alnum, 9 zoned, P packed,
B binary, F float, N national, A address, T bit string (PL/I); `?` when unknown."""
B binary, F float, N national, A address, T bit string (PL/I); `?` when unknown.
`inherited` is the enclosing group's USAGE (#4525), used when the item has none."""
if item.attributes is not None: # #3720: a PL/I item
return pli_mapping.item_class(item.usage, item.pic, item.attributes)
usage = (item.usage or "DISPLAY").upper()
usage = (item.usage or inherited or "DISPLAY").upper()
if usage in ("COMP-3", "COMPUTATIONAL-3", "PACKED-DECIMAL"):
return "P"
if usage in ("COMP", "COMPUTATIONAL", "COMP-4", "COMPUTATIONAL-4", "COMP-5", "COMPUTATIONAL-5", "BINARY"):
Expand All @@ -5585,12 +5620,13 @@ def _item_class(item: EngineDataItem) -> str:
return "9"


def _elementary_bytes(item: EngineDataItem) -> Optional[int]:
"""One occurrence's storage width of an elementary item, or None when unknown."""
def _elementary_bytes(item: EngineDataItem, inherited: str | None = None) -> int | None:
"""One occurrence's storage width of an elementary item, or None when unknown.
`inherited` is the enclosing group's USAGE (#4525), used when the item has none."""
if item.attributes is not None: # #3720: a PL/I item (attributes is None for COBOL)
el = pli_mapping.element(item.usage, item.pic, item.attributes, False)
return None if el is None else (el[0] + 7) // 8
usage = (item.usage or "DISPLAY").upper()
usage = (item.usage or inherited or "DISPLAY").upper()
if usage in ("COMP-1", "COMPUTATIONAL-1", "POINTER", "INDEX"):
return 4
if usage in ("COMP-2", "COMPUTATIONAL-2"):
Expand All @@ -5613,7 +5649,7 @@ def _elementary_bytes(item: EngineDataItem) -> Optional[int]:
if any(p not in _PIC_SYMBOLS and p not in item.currency and not _is_currency_sign(p) for p in positions):
return None
digits = sum(1 for p in positions if p == "9")
cls = _item_class(item)
cls = _item_class(item, usage)
if cls == "P":
return digits // 2 + 1
if cls == "B":
Expand Down
19 changes: 0 additions & 19 deletions tests/cobol_mainframe/fact_crosscheck_ledger.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@
"issue": 3362,
"summary": "A GO TO back to its own paragraph (a read loop): the engine drops a unit's transfer to itself like recursion (#3362 contract, ground-truth ledger 'deliberate'); the translator and the key keep it."
},
"engine-group-usage-not-inherited": {
"side": "engine",
"issue": 4525,
"summary": "A group's USAGE (01 X COMP.) is not applied to its elementary items by the engine (GalaxyIR.record_layout reads each item's own USAGE): they are sized as DISPLAY (S9(4) 4 bytes, not 2 COMP; S9(9)V99 11, not 6 COMP-3). The translator inherits it, as IBM COBOL does."
},
"engine-main-line-no-extent": {
"side": "engine",
"issue": 4302,
Expand Down Expand Up @@ -70,24 +65,10 @@
},
"disagreements": {
"aws-mainframe-modernization-carddemo :: edges | app/cbl/CBSTM03A.CBL | translator | 8500-READTRNX-READ -> GO TO 8500-READTRNX-READ": "engine-drops-self-transfer",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP-VARIABLES (record) +16": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP-VARIABLES/CR-CNT @0+4": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP-VARIABLES/CR-JMP @8+4": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP-VARIABLES/TR-CNT @4+4": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP-VARIABLES/TR-JMP @12+4": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP3-VARIABLES (record) +11": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | COMP3-VARIABLES/WS-TOTAL-AMT @0+11": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | PSA-BLOCK (record) +540": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | PSAPTR (record) +4": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | TCB-BLOCK (record) +16": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | engine | TIOT-INDEX (record) +4": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP-VARIABLES (record) +8": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP-VARIABLES/CR-CNT @0+2": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP-VARIABLES/CR-JMP @4+2": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP-VARIABLES/TR-CNT @2+2": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP-VARIABLES/TR-JMP @6+2": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP3-VARIABLES (record) +6": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | COMP3-VARIABLES/WS-TOTAL-AMT @0+6": "engine-group-usage-not-inherited",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | PSA-BLOCK (record) +544": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | PSAPTR (record) +8": "pointer-width",
"aws-mainframe-modernization-carddemo :: offsets | app/cbl/CBSTM03A.CBL | translator | TCB-BLOCK (record) +20": "pointer-width",
Expand Down
6 changes: 3 additions & 3 deletions tests/cobol_mainframe/test_dbcs_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,14 @@ def test_schema_types():
assert parse_cobol_picture("X(10)", False, None)["sql"] == "VARCHAR(10)"


def test_other_group_usages_are_not_pushed_onto_members():
"""#3816 scope: only NATIONAL / DISPLAY-1 groups pass their usage down; a COMP-3 group's members are unchanged."""
def test_other_group_usages_are_pushed_onto_members_too():
"""#4525 (widening #3816's NATIONAL / DISPLAY-1 rule): every group USAGE applies to its members."""
src = """
01 R COMP-3.
05 A PIC S9(5).
"""
records = extract_boundary("cobol", src)["records"]
assert records[1]["usage"] is None
assert records[1]["usage"] == "COMP-3"


def test_national_in_a_value_literal_is_not_a_usage():
Expand Down
Loading
Loading