#!/usr/bin/env python3 """Incremental mode for rtl-ip-replace (Task12 rules 13-16 / Task12 §5). Baseline = the newest backup_/ under the fixed dir, i.e. the previous run's ORIGINAL MATLAB RTL (src_orig/) + rewritten RTL (*.v) + ip/ + cmp/. plan [--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 [--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()