A line diff of two constraint files is noisy: reordered commands, changed whitespace and renamed variables look like changes, and a one-character change in an exception looks the same as one in a comment. Normalising commands and ranking them by what they can do separates the harmless from the dangerous.
old = """create_clock -name clk -period 2.0 [get_ports clk]
set_input_delay 0.5 -clock clk [get_ports din]
set_false_path -from [get_ports rst_n]
set_clock_uncertainty 0.1 [get_clocks clk]"""
new = """create_clock -name clk -period 1.8 [get_ports clk]
set_clock_uncertainty 0.1 [get_clocks clk]
set_input_delay 0.5 -clock clk [get_ports din]
set_false_path -from [get_clocks clk] -to [get_clocks clk2]
set_multicycle_path 2 -setup -from [get_pins u_a/q] -to [get_pins u_b/d]"""
risk = {"create_clock": 3, "create_generated_clock": 3, "set_false_path": 3, "set_multicycle_path": 3,
"set_clock_uncertainty": 2, "set_input_delay": 2, "set_output_delay": 2}
def norm(text):
return {" ".join(line.split()) for line in text.splitlines() if line.strip()}
a, b = norm(old), norm(new)
removed, added = sorted(a - b), sorted(b - a)
assert len(removed) == 2 and len(added) == 3
def score(lines):
return sorted(((risk.get(l.split()[0], 1), l.split()[0]) for l in lines), reverse=True)
assert score(removed) == [(3, "set_false_path"), (3, "create_clock")]
assert score(added) == [(3, "set_multicycle_path"), (3, "set_false_path"), (3, "create_clock")]
assert max(r for r, _ in score(added) + score(removed)) == 3
unchanged = a & b
assert len(unchanged) == 2 # reordering alone did not register as a change
print("PASS: removed", len(removed), "added", len(added), "unchanged", len(unchanged), "; all four logical changes are high risk")Change | Command | Risk | Why |
|---|---|---|---|
Clock period 2.0 to 1.8 ns | create_clock | High | Changes every check for that clock |
False path from rst_n removed | set_false_path | High | Exceptions change which paths are checked |
New false path between clocks | set_false_path | High | Broad exceptions can hide real paths |
New multicycle setup | set_multicycle_path | High | Relaxes setup, and needs a hold counterpart |
Reordered uncertainty and input delay | — | None | Same commands in a different order |
The normalised comparison shows that the clock period changed, one false path was removed, one was added with a different scope, and a multicycle path was added: five changed lines but four logical changes, all high-risk, and each deserves a reviewer. The reordering of the uncertainty and input delay lines, which a line diff would show as about two changes, correctly registers as no change here because both lines come after the clock definition and neither overrides the other. Order must still be checked separately: a command that references an object before its creation matches nothing, and later commands can override earlier ones, such as case analysis or a second set_input_delay without -add_delay replacing the first. A real tool must also normalise variable expansion and object patterns; this example only normalises whitespace and order, which is already a large improvement over a text diff.
Constraint changes are reviewed from a semantic diff, ranked by command risk.
Any added or changed exception, clock or generated clock needs an approver who is not the author.
A new multicycle setup comes with its hold exception in the same change, or an explanation.
The multicycle setup: its hold exception (N-1) must be added at the same time, or hold will move by a period.