Files
bowei ea4541ee90 sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev
- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合
  (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级)
- rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH /
  XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示
  去设 skills_env.md 的 VIVADO_PATH
- simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具
  check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证)
- sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
2026-07-27 10:39:53 +08:00

228 lines
9.9 KiB
Python

#!/usr/bin/env python3
"""Incremental mode for rtl-ip-replace (Task12 rules 13-16 / Task12 §5).
Baseline = the newest backup_<ts>/ under the fixed dir, i.e. the previous run's
ORIGINAL MATLAB RTL (src_orig/) + rewritten RTL (*.v) + ip/ + cmp/.
plan <HDLSRC> <OUT> [--apply] [--full] [--baseline DIR]
Run right after backup_outdir.py. Classifies every source file against the
baseline snapshot; --apply copies back the rewritten .v of files whose
MATLAB source is byte-identical (nothing to redo for them).
finalize <HDLSRC> <OUT> [--baseline DIR]
Run after the transform scripts (step 2), BEFORE create_ip / cmp sim.
Writes TRANSFORM.diff, compares it + ip_manifest + .coe + scripts_sha1
against the baseline, and decides whether step 3 (IP gen) and step 4
(cycle-true sim) may be skipped. Decisions land in incr_plan.json and are
appended to PROVENANCE.log.
Skipping the sim is only allowed when the rewrite this run performed is
byte-for-byte the same rewrite the baseline PASS was proven on (Task12 §5.4).
"""
import argparse, json, os, shutil, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ipflow_common import (SRC_ORIG, TRANSFORM_DIFF, append_provenance, # noqa: E402
scripts_sha1, sha1_file, snapshot_sources, src_files)
PLAN = 'incr_plan.json'
def find_baseline(out, explicit=None):
if explicit:
return os.path.abspath(explicit)
cands = sorted(d for d in os.listdir(out)
if d.startswith('backup_') and os.path.isdir(os.path.join(out, d)))
for d in reversed(cands): # newest usable one
if os.path.isdir(os.path.join(out, d, SRC_ORIG)):
return os.path.join(out, d)
return None
def read_sha(p):
return sha1_file(p) if os.path.exists(p) else None
def baseline_cmp_pass(base):
p = os.path.join(base, 'cmp', 'cmp_result.txt')
if not os.path.exists(p):
return False, 'no cmp_result.txt in baseline'
first = open(p, errors='replace').readline().strip()
return first.startswith('PASS'), first
def baseline_scripts_sha1(base):
p = os.path.join(base, 'PROVENANCE.log')
if os.path.exists(p):
for ln in open(p, errors='replace'):
if ln.startswith('scripts_sha1:'):
return ln.split(':', 1)[1].strip()
return None
def save(out, plan):
json.dump(plan, open(os.path.join(out, PLAN), 'w'), indent=2)
def load(out):
p = os.path.join(out, PLAN)
return json.load(open(p)) if os.path.exists(p) else {}
def cmd_plan(a):
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
os.makedirs(out, exist_ok=True)
snapshot_sources(hdlsrc, out) # this run's baseline-to-be (rule 14)
base = None if a.full else find_baseline(out, a.baseline)
plan = {'baseline': base, 'incremental': bool(base), 'files': {}, 'reasons': []}
if not base:
plan['reasons'].append('forced --full' if a.full else 'no usable baseline (backup_*/src_orig)')
save(out, plan)
print('FULL run: ' + plan['reasons'][0])
return
bsrc = os.path.join(base, SRC_ORIG)
new, old = src_files(hdlsrc), src_files(bsrc)
for fn in new:
if fn not in old:
plan['files'][fn] = 'new'
elif read_sha(os.path.join(hdlsrc, fn)) == read_sha(os.path.join(bsrc, fn)):
plan['files'][fn] = 'same'
else:
plan['files'][fn] = 'changed'
for fn in old:
if fn not in new:
plan['files'][fn] = 'removed'
same = [f for f, c in plan['files'].items() if c == 'same']
all_same = bool(same) and all(c == 'same' for c in plan['files'].values())
plan['all_same'] = all_same
applied = []
if a.apply:
for fn in same:
src = os.path.join(base, fn) # the REWRITTEN file
if os.path.exists(src):
shutil.copy(src, os.path.join(out, fn))
applied.append(fn)
# .coe / manifests are only safe to take wholesale when NOTHING changed;
# otherwise the transform scripts must re-derive them (a stale .coe of a
# table that moved would silently survive).
extra = (['ip_manifest.tcl', 'ip_manifest.json', 'manifest.json']
+ [f for f in sorted(os.listdir(base)) if f.endswith('.coe')]) if all_same else []
for fn in extra:
s, d = os.path.join(base, fn), os.path.join(out, fn)
if os.path.exists(s) and not os.path.exists(d):
shutil.copy(s, d)
applied.append(fn)
plan['reused_from_baseline'] = applied
save(out, plan)
ok, first = baseline_cmp_pass(base)
print(f'baseline: {base}')
print(' cmp_result: ' + (first if first else 'MISSING'))
for c in ('same', 'changed', 'new', 'removed'):
fs = [f for f, k in plan['files'].items() if k == c]
if fs:
print(f' {c:8s}: {" ".join(sorted(fs))}')
if applied:
print(f' applied {len(applied)} file(s) from baseline (skip transform for them)')
if plan['all_same']:
print(' ALL MATLAB sources unchanged -> step 2 (transform scripts) can be skipped entirely')
if not ok:
print(' NOTE: baseline has no PASS -- cycle-true sim CANNOT be inherited')
def cmd_finalize(a):
from ipflow_common import write_transform_diff
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
write_transform_diff(out)
plan = load(out)
if plan and not plan.get('incremental'):
base = None # a full run was decided in `plan` -- honour it
else:
base = plan.get('baseline') or find_baseline(out, a.baseline)
blockers, ip_reuse, sim_inherit = [], False, False
if not base:
blockers.append('full run (no baseline / --full): everything regenerated')
else:
if any(c in ('new', 'removed') for c in plan.get('files', {}).values()):
blockers.append('source file set changed (new/removed)')
d_new = read_sha(os.path.join(out, TRANSFORM_DIFF))
d_old = read_sha(os.path.join(base, TRANSFORM_DIFF))
if d_old is None:
blockers.append('baseline has no TRANSFORM.diff')
elif d_new != d_old:
blockers.append('TRANSFORM.diff differs from baseline (rewrite is not the same one)')
m_new = read_sha(os.path.join(out, 'ip_manifest.tcl'))
m_old = read_sha(os.path.join(base, 'ip_manifest.tcl'))
coe_new = {f: read_sha(os.path.join(out, f)) for f in os.listdir(out) if f.endswith('.coe')}
coe_old = {f: read_sha(os.path.join(base, f)) for f in os.listdir(base) if f.endswith('.coe')}
ip_same = (m_new is not None and m_new == m_old and coe_new == coe_old)
if not ip_same:
blockers.append('ip_manifest/.coe differ from baseline')
have_ip = os.path.isdir(os.path.join(out, 'ip')) or os.path.isdir(os.path.join(base, 'ip'))
if ip_same and not have_ip: # Task12 §5.5: an incomplete baseline may not be inherited
ip_same = False
blockers.append('baseline has no ip/ directory')
s_now, s_old = scripts_sha1(), baseline_scripts_sha1(base)
if s_old != s_now:
blockers.append(f'skill scripts changed ({s_old} -> {s_now})')
ok, first = baseline_cmp_pass(base)
if not ok:
blockers.append('baseline cmp_result is not PASS')
ip_reuse = ip_same and not any(b.startswith(('no baseline', 'source file set')) for b in blockers)
sim_inherit = not blockers
if ip_reuse and not os.path.isdir(os.path.join(out, 'ip')) \
and os.path.isdir(os.path.join(base, 'ip')):
shutil.copytree(os.path.join(base, 'ip'), os.path.join(out, 'ip'))
for f in ('ip_manifest.json',):
if os.path.exists(os.path.join(base, f)) and not os.path.exists(os.path.join(out, f)):
shutil.copy(os.path.join(base, f), os.path.join(out, f))
plan.update({'ip_reuse': ip_reuse, 'sim_inherit': sim_inherit, 'blockers': blockers})
save(out, plan)
files = plan.get('files', {})
reused = sorted(f for f, c in files.items() if c == 'same')
regen = sorted(f for f, c in files.items() if c != 'same')
append_provenance(out, [
'',
'# --- incremental run (Task12 rules 13-16) ---',
'incremental: ' + ('on' if base else 'off'),
'baseline: ' + (os.path.basename(base) + '/' if base else '-'),
'reused_files: ' + (' '.join(reused) or '-'),
'regenerated_files: ' + (' '.join(regen) or '-'),
'ip_reuse: ' + ('full' if ip_reuse else 'none'),
'cmp_sim: ' + (f'INHERITED PASS from {os.path.basename(base)}/cmp/cmp_result.txt'
if sim_inherit else 'MUST RUN'),
'blockers: ' + ('; '.join(blockers) or '-'),
])
print('ip_reuse :', 'YES (skip step 3 create_ip)' if ip_reuse else 'no (run step 3)')
print('sim_inherit :', 'YES (skip step 4 cycle-true sim, PASS inherited from %s)'
% os.path.basename(base) if sim_inherit else 'no (run step 4)')
for b in blockers:
print(' blocker:', b)
def main():
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest='cmd', required=True)
for name in ('plan', 'finalize'):
s = sub.add_parser(name)
s.add_argument('hdlsrc')
s.add_argument('outdir')
s.add_argument('--baseline', help='explicit baseline dir (default: newest backup_*)')
if name == 'plan':
s.add_argument('--apply', action='store_true',
help='copy back rewritten files whose source is unchanged')
s.add_argument('--full', action='store_true', help='force a full run')
a = p.parse_args()
(cmd_plan if a.cmd == 'plan' else cmd_finalize)(a)
if __name__ == '__main__':
main()