merge local cobol_testgen improvements into v3 shared modules

- cond.py: SQLCODE/SQLSTATE handling, alphanumeric >/< boundary fix
- output.py: termination tracking, db_input support, _is_field_assigned filter
- coverage.py: mark_from_gcov, THRU support, KeyError protection
- gcov.py: new file (dependency for coverage.py)
- grammar.lark: multi-segment PIC support
- read.py: SQL INCLUDE resolution, DECLARE TABLE parsing, * comment fix
- core.py: SQL parsing, blocked_names, keyword list
- design.py: multi-sentinel, THRU ranges, PERFORM VARYING last iteration
- __init__.py: local main() + v3 API functions, guarded imports

All 6 ZAN programs verified passing through v3 pipeline
This commit is contained in:
hangshuo652
2026-06-23 22:38:17 +08:00
parent e5ab3baa46
commit 7fb9304212
9 changed files with 1595 additions and 326 deletions
+451 -47
View File
@@ -8,12 +8,52 @@ from .core import trace_to_root, invert_through_chain, propagate_assignments, _b
logger = logging.getLogger(__name__)
_STOP = ('__STOP__', '', None, True)
_MAX_PATHS = 500
_STOP_EXIT_PERFORM = ('__STOP_EXIT_PERFORM__', '', None, True)
_STOP_SENTINEL = ('__STOP__', '', None, True)
_ABEND_SENTINEL = ('__ABEND__', '', None, True)
_SENTINELS_ALL = {_STOP_EXIT_PERFORM, _STOP_SENTINEL, _ABEND_SENTINEL}
_ABEND_PROGRAMS = {'ABENDPGM'}
def extend_abend_programs(names: list[str]):
_ABEND_PROGRAMS.update(n.upper() for n in names)
_MAX_PATHS = 10000
def _is_sentinel(c):
return c is _STOP_EXIT_PERFORM or c is _STOP_SENTINEL or c is _ABEND_SENTINEL
def _hashable_cons(cons):
"""将约束列表转为可哈希形式(列表值转tuple)用于签名去重。"""
result = []
for c in cons:
if len(c) == 4:
field, op, val, want = c
if isinstance(val, list):
val = tuple(val)
result.append((field, op, val, want))
else:
result.append(c)
return result
def _filter_stop(cons):
return [c for c in cons if c is not _STOP]
"""Legacy: strip all sentinel markers. 供旧测试代码使用。"""
return [c for c in cons if not _is_sentinel(c)]
def get_term_type(cons):
"""提取终止类型,返回 (filtered_cons, term_type)."""
remaining = []
term = 'normal'
for c in cons:
if c is _ABEND_SENTINEL:
term = 'abend'
elif _is_sentinel(c):
pass
else:
remaining.append(c)
return remaining, term
def _cap_paths(paths):
@@ -29,11 +69,11 @@ def _cap_paths_fair(new_active, child_paths):
k = len(child_paths)
if k <= 1:
return new_active[:_MAX_PATHS]
# 分离 STOP 路径(不参与组合,直接保留)
stop_paths = [(p, a) for p, a in new_active if any(c is _STOP for c in p)]
combined = [(p, a) for p, a in new_active if not any(c is _STOP for c in p)]
# 分离 sentinel 路径(不参与组合,直接保留)
stop_paths = [(p, a) for p, a in new_active if any(_is_sentinel(c) for c in p)]
combined = [(p, a) for p, a in new_active if not any(_is_sentinel(c) for c in p)]
n_pred = len(combined) // k
result = list(stop_paths)
result = []
if n_pred <= 1:
result.extend(combined[:_MAX_PATHS - len(result)])
return result[:_MAX_PATHS]
@@ -75,24 +115,29 @@ def enum_paths(node, fields):
for child in node.children:
child_paths = _cap_paths(enum_paths(child, fields))
if not child_paths:
break
continue
new_active = []
covered_sigs = set()
for p_cons, p_assign in paths:
if any(c is _STOP for c in p_cons):
if any(_is_sentinel(c) for c in p_cons):
new_active.append((p_cons, p_assign))
continue
for cp_cons, cp_assign in child_paths:
merged = {}
for d in (p_assign, cp_assign):
for k, v in d.items():
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
merged_cons = p_cons + list(cp_cons)
new_active.append((merged_cons, merged))
if len(new_active) >= _MAX_PATHS:
sig = frozenset(_hashable_cons(merged_cons))
if sig not in covered_sigs:
covered_sigs.add(sig)
merged = {}
for d in (p_assign, cp_assign):
for k, v in d.items():
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
new_active.append((merged_cons, merged))
if not new_active:
for pc, pa in paths:
if not any(_is_sentinel(c) for c in pc):
new_active.append((pc, dict(pa)))
break
if len(new_active) >= _MAX_PATHS:
break
paths = _cap_paths_fair(new_active, child_paths)
paths = new_active
return paths
elif isinstance(node, BrIf):
@@ -186,6 +231,14 @@ def enum_paths(node, fields):
constraints.append((cond.field, cond.op, cond.value, True))
paths.append((constraints + sp_cons, sp_assign))
prior_false_sets.append([(cond.field, cond.op, cond.value, False)])
elif cond and isinstance(cond, CondNot) and isinstance(cond.child, CondLeaf) and is_field(cond.child.field, fields):
leaf = cond.child
sub = _cap_paths(enum_paths(seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
constraints = [c for pf in prior_false_sets for c in pf]
constraints.append((leaf.field, leaf.op, leaf.value, False))
paths.append((constraints + sp_cons, sp_assign))
prior_false_sets.append([(leaf.field, leaf.op, leaf.value, True)])
elif cond:
leaves = collect_leaves(cond)
if leaves and all(is_field(l.field, fields) for l in leaves):
@@ -232,13 +285,36 @@ def enum_paths(node, fields):
paths = []
for value, seq in node.when_list:
sub = _cap_paths(enum_paths(seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, '=', value, True)] + sp_cons, sp_assign))
thru_m = re.match(r'^(\d+)\s+THRU\s+(\d+)$', str(value), re.IGNORECASE)
if thru_m and not node.subjects:
low, high = thru_m.group(1), thru_m.group(2)
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, '>=', low, True), (node.subject, '<=', high, True)] + sp_cons, sp_assign))
paths.append(([(node.subject, '<=', high, True), (node.subject, '>=', low, True)] + sp_cons, sp_assign))
else:
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, '=', value, True)] + sp_cons, sp_assign))
if node.has_other:
case_vals = [v for v, _ in node.when_list]
sub = _cap_paths(enum_paths(node.other_seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, 'not_in', case_vals, True)] + sp_cons, sp_assign))
thru_found = False
for v, _ in node.when_list:
thru_m = re.match(r'^(\d+)\s+THRU\s+(\d+)$', str(v), re.IGNORECASE)
if thru_m and not node.subjects:
thru_found = True
low_int, high_int = int(thru_m.group(1)), int(thru_m.group(2))
for sp_cons, sp_assign in (sub or [([], {})]):
a_low = dict(sp_assign)
a_low[node.subject] = [{'type': 'move_literal', 'literal': str(max(0, low_int - 1))}]
low_cons = [(node.subject, 'not_in', [thru_m.group(1), thru_m.group(2)], True)]
paths.append((low_cons + sp_cons, a_low))
a_high = dict(sp_assign)
a_high[node.subject] = [{'type': 'move_literal', 'literal': str(high_int + 1)}]
high_cons = [(node.subject, 'not_in', [thru_m.group(1), thru_m.group(2)], True)]
paths.append((high_cons + sp_cons, a_high))
if not thru_found:
case_vals = [v for v, _ in node.when_list]
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, 'not_in', case_vals, True)] + sp_cons, sp_assign))
return paths
elif isinstance(node, BrSearch):
@@ -247,7 +323,10 @@ def enum_paths(node, fields):
elif isinstance(node, BrPerform):
if node.perf_type in ('para', 'thru'):
if node.body_seq:
return enum_paths(node.body_seq, fields)
paths = enum_paths(node.body_seq, fields)
# EXIT PERFORM 只在 PERFORM 体内有效,剥离后不影响后续 BrSeq 组合
paths = [([c for c in cons if c is not _STOP_EXIT_PERFORM], a) for cons, a in paths]
return paths
return [([], {})]
elif node.perf_type in ('until', 'para_until', 'varying', 'para_varying'):
# 尝试单条件(现有逻辑)
@@ -256,7 +335,9 @@ def enum_paths(node, fields):
field, op, val = parsed
paths = []
false_sub = _cap_paths(enum_paths(node.body_seq, fields))
false_sub = [([c for c in cons if c is not _STOP_EXIT_PERFORM], a) for cons, a in false_sub]
for sp_cons, sp_assign in (false_sub or [([], {})]):
body_assign = dict(sp_assign)
# PERFORM VARYING: 将 FROM 值作为 MOVE 赋值加入 Enter 路径
if node.varying_from and node.varying_var:
is_fld = any(f['name'] == node.varying_from for f in fields) if fields else False
@@ -268,6 +349,40 @@ def enum_paths(node, fields):
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
sp_assign = merged
paths.append(([(field, op, val, False)] + sp_cons, sp_assign))
# PERFORM VARYING: 末次迭代路径(下标=MAX)
if node.varying_from and node.varying_var and op in ('>', '>=', '<', '<=', '='):
try:
if op == '>':
max_val = int(val)
elif op == '>=':
max_val = int(val) - 1
elif op == '<':
max_val = int(val)
elif op == '<=':
max_val = int(val) + 1
elif op == '=':
by_str = str(node.varying_by or '1')
if by_str.lstrip('-').isdigit() and int(by_str) < 0:
max_val = int(val) + 1
else:
max_val = int(val) - 1
from_val = int(node.varying_from)
by_str = str(node.varying_by or '1')
if by_str.lstrip('-').isdigit() and int(by_str) < 0:
ok = max_val <= from_val
else:
ok = max_val >= from_val
if ok:
max_asgn = {'type': 'move_literal', 'literal': str(max_val)}
max_assign = {node.varying_var: [max_asgn]}
merged_max = {}
for d in (max_assign, body_assign):
for k, v in d.items():
merged_max.setdefault(k, []).extend(v if isinstance(v, list) else [v])
the_cons = [(field, op, val, False)]
paths.append((the_cons + sp_cons, merged_max))
except (ValueError, TypeError):
pass
paths.append(([(field, op, val, True)], {}))
return paths
# 尝试复合条件(AND/OR
@@ -279,6 +394,7 @@ def enum_paths(node, fields):
if sets:
paths = []
false_sub = _cap_paths(enum_paths(node.body_seq, fields))
false_sub = [([c for c in cons if c is not _STOP_EXIT_PERFORM], a) for cons, a in false_sub]
for sp_cons, sp_assign in (false_sub or [([], {})]):
# PERFORM VARYING: 将 FROM 值作为 MOVE 赋值加入 Enter 路径
if node.varying_from and node.varying_var:
@@ -301,14 +417,18 @@ def enum_paths(node, fields):
return [([], {})]
elif isinstance(node, CallNode):
if node.program_name in _ABEND_PROGRAMS:
return [([_ABEND_SENTINEL], {})]
return [([], {})]
elif isinstance(node, ExitNode):
return [([_STOP], {})]
if node.exit_type == 'PERFORM':
return [([_STOP_EXIT_PERFORM], {})]
return [([_STOP_SENTINEL], {})]
elif isinstance(node, GoTo):
paths = enum_paths(node.body_seq, fields)
return [([_STOP] + c, a) for c, a in paths]
return [([_STOP_SENTINEL] + c, a) for c, a in paths]
return [([], {})]
@@ -335,7 +455,7 @@ def seq_date(seq_num: int) -> str:
def _is_date_field(name: str) -> bool:
patterns = [r'DATE', r'YYMMDD', r'YYYYMM', r'YEAR', r'MONTH', r'DAY']
patterns = [r'DATE', r'YYMMDD', r'YYYYMM']
for p in patterns:
if re.search(p, name.upper()):
return True
@@ -401,13 +521,12 @@ def _children_of(group_name: str, fields: list) -> list:
def _make_numeric_value(idx: int, record_num: int, total_digits: int) -> str:
max_val = 10 ** total_digits - 1
max_val = 10 ** total_digits
for step in (100, 10, 1):
val = idx * step + record_num
if val < 10 ** total_digits:
return str(min(val, max_val)).zfill(total_digits)
return str(min(record_num, max_val)).zfill(total_digits)
return str(record_num).zfill(total_digits)
if val < max_val:
return str(val).zfill(total_digits)
return str(record_num % max_val).zfill(total_digits)
def _make_alpha_value(idx: int, record_num: int, length: int) -> str:
@@ -548,6 +667,16 @@ def _check_constraint_satisfied(rec, field_name, operator, value, want_true, fie
return eq == want_true
elif operator == '<>':
return (not eq) == want_true
elif operator in ('>', '<', '>=', '<='):
if operator == '>':
ok = s_val > s_target
elif operator == '<':
ok = s_val < s_target
elif operator == '>=':
ok = s_val >= s_target
elif operator == '<=':
ok = s_val <= s_target
return ok == want_true
return True
return False
@@ -625,6 +754,95 @@ def _apply_arith_constraint(rec, field_name, operator, value, want_true, fields)
rec[right_field] = pick
def _inc_str(s, length):
s = str(s).strip()
try:
r = str(int(s) + 1).zfill(length)
return r if len(r) <= length else '9' * length
except ValueError:
c = list(str(s).ljust(length)[:length])
for i in range(len(c) - 1, -1, -1):
if c[i] not in ' 9Zz\xff':
c[i] = chr(ord(c[i]) + 1)
break
if c[i] == ' ':
c[i] = '0'
break
if c[i] == '9':
c[i] = '0'
elif c[i] == 'Z':
c[i] = 'A'
elif c[i] == 'z':
c[i] = 'a'
return ''.join(c)
def _dec_str(s, length):
s = str(s).strip()
try:
n = max(0, int(s) - 1)
return str(n).zfill(length)
except ValueError:
c = list(str(s).ljust(length)[:length])
for i in range(len(c) - 1, -1, -1):
if c[i] not in ' 0Aa\x00':
c[i] = chr(ord(c[i]) - 1)
break
if c[i] == ' ':
break
if c[i] == '0':
c[i] = '9'
elif c[i] == 'A':
c[i] = ' '
elif c[i] == 'a':
c[i] = ' '
return ''.join(c)
def _reconcile_unstring_fields(rec, left_field, operator, right_field, want_true,
fields, left_chain, assignments, path_assign):
right_root, right_chain = trace_to_root(right_field, assignments, fields, path_assign)
if right_root not in rec:
logger.debug(f"字段间比较协调:右侧根 {right_root} 不在 rec,跳过")
return
all_entries = (left_chain or []) + (right_chain or [])
for _, asgn in all_entries:
if asgn.get('type') not in ('move', 'unstring_split'):
logger.debug(f"字段间比较协调:链含非 MOVE 类型 {asgn.get('type')},跳过")
return
left_val = str(rec.get(left_field, ''))
if not left_val.strip():
logger.debug(f"字段间比较协调:左侧 {left_field} 无值,跳过")
return
length = 0
for f in fields:
if f['name'] == right_root:
length = f.get('pic_info', {}).get('length', 0)
break
if length == 0:
length = len(left_val)
if operator in ('>=', '<='):
if want_true:
right_val = left_val
else:
right_val = _inc_str(left_val, length) if operator == '>=' else _dec_str(left_val, length)
elif operator in ('>', '<'):
if want_true:
right_val = _dec_str(left_val, length) if operator == '>' else _inc_str(left_val, length)
else:
right_val = left_val
elif operator == '=':
right_val = left_val if want_true else _inc_str(left_val, length)
elif operator == '<>':
right_val = _inc_str(left_val, length) if want_true else left_val
else:
return
rec[right_root] = right_val[:length] if right_val else right_val
logger.debug(f"字段间比较协调:{left_field}={left_val} {operator} {right_field} -> {right_root}={rec[right_root]} (want={want_true})")
def apply_constraint(rec, field_name, operator, value, want_true, fields, assignments=None, path_assign=None):
# 标准化字段名:去除括号内空格(WS-CELL ( 1, 1 ) → WS-CELL(1,1)
field_name = re.sub(r'\s*([(),])\s*', r'\1', field_name)
@@ -659,6 +877,7 @@ def apply_constraint(rec, field_name, operator, value, want_true, fields, assign
apply_constraint(rec, parent_name, operator, value, want_true, fields, assignments, path_assign)
return
break
chain = None
if assignments:
root_var, chain = trace_to_root(field_name, assignments, fields, path_assign)
if root_var != field_name:
@@ -666,8 +885,41 @@ def apply_constraint(rec, field_name, operator, value, want_true, fields, assign
if any(f['name'] == new_field_name for f in fields):
field_name, operator, value = new_field_name, new_op, new_val
# 字段间比较:在 satisfied check 前解析/处理
if any(f['name'] == value for f in fields):
resolved_literal = None
for f in fields:
if f['name'] == value and f.get('value') is not None:
resolved_literal = str(f['value']).strip("'").strip('"')
break
if resolved_literal is not None:
value = resolved_literal
elif chain is not None and assignments:
_reconcile_unstring_fields(rec, field_name, operator, value, want_true,
fields, chain, assignments, path_assign)
return
elif re.search(r'[+\-*/]', field_name):
_apply_arith_constraint(rec, field_name, operator, value, want_true, fields)
return
else:
logger.debug(f"字段间比较约束跳过:{field_name} {operator} {value}")
return
# 如果当前值已满足该约束,跳过覆盖(保持先前约束的一致性)
# 但零值时强制使用边界值(非 0/非 min)
if _check_constraint_satisfied(rec, field_name, operator, value, want_true, fields):
cur = str(rec.get(field_name, '')).strip('0')
if (cur == '' or cur == '.') and (
(operator in ('>', '>=') and not want_true) or
(operator in ('<', '<=') and want_true)
):
for f in fields:
if f['name'] == field_name:
pi = f.get('pic_info', {})
if pi.get('type') == 'numeric':
val = satisfying_value(pi, operator, value, want_true)
rec[field_name] = val
return
return
if operator == 'not_in':
@@ -687,13 +939,6 @@ def apply_constraint(rec, field_name, operator, value, want_true, fields, assign
rec[field_name] = str(n).zfill(pi.get('digits', 0) + pi.get('decimal', 0))
return
return
# 字段间比较(值侧也是字段名)
if any(f['name'] == value for f in fields):
if re.search(r'[+\-*/]', field_name):
_apply_arith_constraint(rec, field_name, operator, value, want_true, fields)
else:
logger.debug(f"字段间比较约束跳过:{field_name} {operator} {value}")
return
for f in fields:
if f['name'] == field_name:
pi = f.get('pic_info', {})
@@ -738,6 +983,31 @@ def sync_redefined_fields(rec, fields):
def apply_occurs_depending(rec, fields):
"""根据 OCCURS DEPENDING ON 变量的当前值,清零超范围的下标字段。"""
# Phase 1: 将零值的 DEPENDING ON 变量设为最大下标
dep_max = {}
for f in fields:
dep_var = f.get('occurs_depending')
if not dep_var:
continue
m = re.search(r'\((\d+)\)$', f['name'])
if m:
sub = int(m.group(1))
if sub > dep_max.get(dep_var, 0):
dep_max[dep_var] = sub
for dep_var, max_sub in dep_max.items():
try:
cur_val = int(float(str(rec.get(dep_var, '0'))))
except (ValueError, TypeError):
cur_val = 0
if cur_val == 0:
for f in fields:
if f['name'] == dep_var:
pi = f.get('pic_info', {})
digits = pi.get('digits', 0) + pi.get('decimal', 0)
if digits > 0:
rec[dep_var] = str(max_sub).zfill(digits)
break
# Phase 2: 清零超范围的下标字段
for f in fields:
dep_var = f.get('occurs_depending')
if not dep_var:
@@ -805,7 +1075,10 @@ def _enum_search_paths(node, fields):
base = re.sub(r'\s*\(.*?\)\s*$', '', cond_tree.field)
matching_val = cond_tree.value
elem_key = f'{base}({i + 1})'
extra_assign[elem_key] = [{'type': 'move_literal', 'literal': matching_val}]
if any(f['name'] == matching_val for f in fields):
extra_assign[elem_key] = [{'type': 'move', 'source_vars': [matching_val]}]
else:
extra_assign[elem_key] = [{'type': 'move_literal', 'literal': matching_val}]
non_match = _non_match_for(cond_tree, fields) or ' '
for j in range(i):
prev_key = f'{base}({j + 1})'
@@ -815,7 +1088,10 @@ def _enum_search_paths(node, fields):
merged_assign = dict(extra_assign)
for k, v in sp_assign.items():
merged_assign.setdefault(k, []).extend(v if isinstance(v, list) else [v])
paths.append((sp_cons, merged_assign))
if cond_tree and isinstance(cond_tree, CondLeaf):
paths.append(([(elem_key, cond_tree.op, matching_val, True)] + sp_cons, merged_assign))
else:
paths.append((sp_cons, merged_assign))
if node.has_at_end:
sub = _cap_paths(enum_paths(node.at_end_seq, fields))
@@ -837,16 +1113,20 @@ def _enum_search_paths(node, fields):
return paths
def generate_records(branch_paths_with_assigns, data_fields, base_assignments=None, file_sec=None):
def generate_records(path_infos, data_fields, base_assignments=None, file_sec=None):
"""生成测试数据记录。
branch_paths_with_assigns: list of (constraints, path_assignments).
path_infos: list of (constraints, path_assignments) 或 (constraints, path_assignments, term_type).
base_assignments: 全局 assignments dict (用于 trace_to_root).
返回: (records, kept_path_cons) — kept_path_cons 是与 records 一一对应的约束。
返回: (records, kept_path_cons, term_types).
"""
# 自动兼容旧 2-tuple 格式
if path_infos and len(path_infos[0]) == 2:
path_infos = [(c, a, 'normal') for c, a in path_infos]
records = []
kept_path_cons = []
if branch_paths_with_assigns:
for seq, (path_cons, path_assign) in enumerate(branch_paths_with_assigns, start=1):
term_types = []
if path_infos:
for seq, (path_cons, path_assign, term_type) in enumerate(path_infos, start=1):
path_cons = _filter_stop(path_cons)
rec = make_base_record(seq, data_fields)
# Pass A: 先传播赋值(MOVE/COMPUTE/READ INTO 等),模拟到决策点前的程序状态
@@ -869,6 +1149,26 @@ def generate_records(branch_paths_with_assigns, data_fields, base_assignments=No
if not _check_constraint_satisfied(rec, root_var, new_op, new_val, want, data_fields):
skip_impossible = True
break
elif field in rec:
asgn_val = path_assign.get(field)
if asgn_val is not None:
asgn_list = asgn_val if isinstance(asgn_val, list) else [asgn_val]
if asgn_list and asgn_list[-1]['type'] == 'move_literal':
cur_val = str(rec.get(field, ''))
if cur_val != '':
pi = next((f.get('pic_info', {}) for f in data_fields if f['name'] == field), {})
if pi.get('type') == 'numeric':
try:
nv = int(float(cur_val))
tv = int(float(str(val)))
ops = {'>': lambda a,b: a > b, '<': lambda a,b: a < b, '=': lambda a,b: a == b, '<>': lambda a,b: a != b, '>=': lambda a,b: a >= b, '<=': lambda a,b: a <= b}
if op in ops:
satisfied = ops[op](nv, tv) == want
if not satisfied:
skip_impossible = True
break
except (ValueError, TypeError):
pass
if skip_impossible:
continue
# Pass B: 约束覆盖(确保决策条件满足,覆盖 MOVE 带来的值)
@@ -886,17 +1186,121 @@ def generate_records(branch_paths_with_assigns, data_fields, base_assignments=No
forward[tgt] = filtered
if forward:
propagate_assignments(rec, forward, data_fields, file_sec=file_sec)
# Pass B.75: COMPUTE 重算(约束修改了 COMPUTE 源字段的值)
if isinstance(path_assign, dict):
compute_only = {}
for tgt, asgn_val in path_assign.items():
asgn_list = asgn_val if isinstance(asgn_val, list) else [asgn_val]
filtered = [a for a in asgn_list if a['type'] == 'compute']
if filtered:
compute_only[tgt] = filtered
if compute_only:
propagate_assignments(rec, compute_only, data_fields, file_sec=file_sec)
# Pass B.8: UNSTRING source reconstruction (targets → source)
if base_assignments:
_reconstruct_unstring_sources(rec, base_assignments, data_fields)
# Pass C: 同步 REDEFINES(确保共享存储一致)
sync_redefined_fields(rec, data_fields)
# Pass D: OCCURS DEPENDING ON — 清零超范围的下标字段
apply_occurs_depending(rec, data_fields)
# Pass E: PIC 长度约束 — 模拟 COBOL 截断语义
for f in data_fields:
name = f['name']
if name in rec and not f.get('is_88') and not f.get('is_filler'):
pi = f.get('pic_info', {})
ftype = pi.get('type', 'unknown')
val = str(rec[name])
if ftype == 'numeric':
total = pi.get('digits', 0) + pi.get('decimal', 0)
if total > 0 and len(val) > total:
rec[name] = val[-total:].zfill(total)
elif ftype in ('alphanumeric', 'alphabetic'):
length = pi.get('length', 0)
if length > 0 and len(val) > length:
rec[name] = val[:length]
records.append(rec)
kept_path_cons.append(path_cons)
term_types.append(term_type)
# Track which fields were explicitly assigned in this path
if isinstance(path_assign, dict):
rec['_assigned_fields'] = set(path_assign.keys())
else:
rec['_assigned_fields'] = set()
if not records:
rec = make_base_record(1, data_fields)
if base_assignments:
propagate_assignments(rec, base_assignments, data_fields, file_sec=file_sec)
if base_assignments:
_reconstruct_unstring_sources(rec, base_assignments, data_fields)
rec['_assigned_fields'] = set()
records.append(rec)
kept_path_cons.append([])
return records, kept_path_cons
term_types.append('normal')
return records, kept_path_cons, term_types
def _reconstruct_unstring_sources(rec, base_assignments, data_fields):
"""Build UNSTRING source field value from comma-separated target values.
After constraints determine target field values, construct the source
string so the COBOL UNSTRING can correctly parse it.
"""
groups = {}
for tgt, asgn_list in base_assignments.items():
for asgn in asgn_list:
if asgn.get('type') == 'unstring_split' and asgn.get('source_vars'):
src = asgn['source_vars'][0]
idx = asgn.get('index', 0)
groups.setdefault(src, []).append((idx, tgt))
for src_var, targets in groups.items():
targets.sort(key=lambda x: x[0])
# Resolve group→child name if source not directly in rec
resolved_src = src_var
if resolved_src not in rec:
grp_level = None
found = False
for f in data_fields:
if not found and f['name'] == resolved_src:
grp_level = f.get('level', 0)
found = True
continue
if found:
if f.get('level', 0) <= grp_level or f.get('level') == 77:
break
if f.get('pic'):
resolved_src = f['name']
break
if resolved_src not in rec:
continue
csv_parts = []
for idx, tgt in targets:
val = rec.get(tgt, '')
csv_parts.append(val if val is not None else '')
csv_value = ','.join(csv_parts)
src_len = 0
for f in data_fields:
if f['name'] == resolved_src:
pi = f.get('pic_info', {})
if pi:
src_len = pi.get('length', 0)
break
if src_len > 0:
csv_value = csv_value.ljust(src_len)[:src_len]
rec[resolved_src] = csv_value
# Also sync to child fields (group→elementary) for FD output consistency
if resolved_src == src_var:
grp_level = None
found = False
for f in data_fields:
if not found and f['name'] == resolved_src:
grp_level = f.get('level', 0)
found = True
continue
if found:
if f.get('level', 0) <= grp_level or f.get('level') == 77:
break
if f.get('pic'):
rec[f['name']] = csv_value
break