| name | synthesis |
| description | Use when running Design Compiler synthesis, analyzing timing/area/power reports, supplementing SDC exceptions, or re-synthesizing after RTL changes; not for power analysis or static timing. |
Synthesis
Your sole responsibility: carry this module's declared timing exceptions into the SDC, converge
Design Compiler against it, and close the run through the synthesis CLI.
Iron Rule
- Write only under
{workdir}. Every injected input location is read-only, as is every other
stage's output.
- Scripts are black boxes, never Read their source. Invoke them per this skill's documented
command lines (flags via
--help); on a non-zero exit act on the documented failure protocol
(stderr, stdout verdict), not the source. Sole exception: debugging a suspected bug in a
script itself.
What you read, and what you edit
<skill> is this skill's own base directory, named on the first line of this file.
{workdir}/dispatch.json carries the inputs table below, so <key> denotes a location and you
read <key>/<subpath>. It also carries scope and caused_by when the kernel knows what changed
since your last run: those narrow which inherited exceptions you re-check and which violations you
triage, never which declarations you render — step 2 carries all of them every round, because the
SDC dc_shell reads is rebuilt every round.
| Path | Use |
|---|
<annotations>/constraint-annotations.json | The sdc block per child: every timing exception and generated clock this RTL implies, in real module names. Its authors declared it and this stage is its only consumer. Schema: skills/rtl-design/references/constraint-annotations.schema.json. |
<rtl>/rtl-files.json | Per-child file layout, which bootstrap turns into scripts/rtl_load.tcl. The RTL itself is under <rtl> too, and step 2 reads it for divider ratios. Schema: skills/rtl-design/references/rtl-files.schema.json. |
<sdc>/constraints/<TOP>.sdc | Clocks and IO delays from specification. bootstrap reads it every round, so a correction here arrives on its own; it is not yours to restate or override. |
<ppa>/ppa.json | The area and slack targets this run is judged against. finalize reads them itself; you read them when deciding which side of a PPA miss is wrong. Schema: skills/specification/references/ppa.schema.json. |
LIB_DB must be in the environment before make: env.sh refuses to run without it, and the
placeholder in scripts/config.tcl is a fallback for a dc_shell started outside the Makefile,
not a second way to set it. Exporting it after step 1 is fine.
One file under {workdir} is yours to edit, and it reaches you holding the previous round's work
rather than the specification SDC:
constraints.sdc: every exception, every library value, and the # notes: that say why.
Treat what is in it as work you inherited. Re-check each exception against this run's reports and
delete one whose path no longer exists, but re-deriving a set you already have costs a full
re-synthesis per round. Everything else under {workdir} is produced by the tools you invoke, and
finalize enumerates it into artifacts[] for you.
Workflow
1. Deploy
Run bootstrap to lay down the run scaffold:
python3 <skill>/scripts/synthesis/__main__.py bootstrap --workdir {workdir} [--top <TOP>]
It generates scripts/rtl_load.tcl and scripts/config.tcl from the rtl-design file layout, and
assembles constraints.sdc from the specification SDC plus your inherited constraints.local.sdc.
It aborts when {workdir}/Makefile already exists (the
kernel-written dispatch.json does not count as "deployed"), and reads the top-module name from
manifest.module when --top is omitted. Non-zero exit: stderr names the cause, and nothing was
deployed, so the retry is not blocked. make is the interface to everything it deployed.
2. Constrain
Union the sdc block across every child of <annotations>/constraint-annotations.json and render
all three categories into constraints.sdc before you run anything:
| sidecar key | what it carries | what you write |
|---|
create_generated_clock | {module, pin} — where a divider or PLL output leaves that child's RTL | create_generated_clock on that pin. -source is the master clock the specification SDC already declares; the divide factor comes from that module's RTL under <rtl> |
set_multicycle_path | one free-form description per exception its author knows the design needs | set_multicycle_path naming the real startpoint / endpoint |
set_false_path | the same, for architecturally unreachable paths | set_false_path naming the real startpoint / endpoint |
Every category is always present, so an empty array is that child's claim to have none. These are
design facts their authors declared rather than suppressions you are guessing at, which is why
all three go in one pass: each one you leave for dc_shell to surface costs a full synthesis
iteration to discover, and this sidecar is the only place rtl-design can state them.
Transcribe, never invent. lint-cdc reads this same sidecar for its SGDC side, so an exception you
add on your own authority has no counterpart there and the two constraint sets diverge silently.
A path nobody declared is step 3's to report, not yours to except.
Everything you write goes in constraints.local.sdc, which bootstrap sources after the seed.
Tcl takes the last assignment, so settling one of the seed's placeholders is a line here, not an
edit there: the set_clock_uncertainty -setup / -hold values its ;# notes flag, and
set_drive / set_load, which it carries for no port — add those only where the IO cell library
documents them. Anything you leave at a placeholder, and anything you decide not to add, needs a
# notes: line saying why: this file is promoted, and the next reader cannot tell a measured
margin from a default or an omission from an oversight.
constraints.sdc is the file dc_shell reads and is generated every round from the seed plus
your local file. Editing it is pointless — the next round overwrites it — and that is what lets a
corrected clock upstream reach the tool without touching what you measured.
3. Converge
make synthesis runs dc_shell and outlives the foreground Bash timeout. Launch it as one
detached background job (run_in_background=True) from {workdir}, stay in this turn until it
exits, then read run.log once. The Makefile tees that log, so poll it — nothing resumes a
subagent when a job it started finishes.
Read the violated paths in reports/timing_setup.rpt, keeping each one's startpoint, endpoint and
slack. Step 2 already carried in every exception the design declares, so a path that is still
violating is one of two things:
- A declaration you rendered wrong — the description named a path and your SDC command does
not match it. Fix the command and re-run.
- A path nobody declared — a real violation. Stop iterating and go to step 4. Its negative
slack fails the
timing_slack_ns target on its own, so the gate writes the violations[] row;
what it cannot write is who must fix it.
Excepting the second kind here on your own judgement is the one way this stage can except its way
to a passing PPA verdict. If the path really is multicycle or false, its author is the one who
says so: name rtl-design in step 4 and it comes back declared, in the sidecar lint-cdc reads too.
A non-zero make ends the run with nothing to grade, so go straight to step 4 carrying the cause
you read in run.log.
4. Close
Run finalize to write the envelope. Every run ends here, a dc_shell that never reached the
reports included, and you never hand-assemble it:
python3 <skill>/scripts/synthesis/__main__.py finalize \
--workdir {workdir} [--fix-owner <rule>] \
[--fail-reason "<cause>"]
It judges the PPA gate (worst setup slack = min of Critical Path Slack across every
clock-group block; area = Total cell area) against the targets it reads from <ppa>/ppa.json
itself — area_um2 and timing_slack_ns only, an absent file or dim leaving that dimension
ungated — records both measurements as stage_specific.ppa_actual[], reads the DC version off the
report header, and enumerates artifacts[]. A clean gate
is not enough for a pass: all three of out/*_syn.{v,sdc,sdf} must be on disk, and an incomplete
set is a tooling fail rather than a promoted synthesis the downstream stages cannot read.
The flags carry what the reports cannot:
--fail-reason, which fills stage_specific.fail_reason, when dc_shell produced nothing
gradeable: no license, an analyze / elaborate / link / check_design / compile_ultra
abort, or
a crash after the reports landed. You are the one who read run.log. Supplying it is itself the
declaration of failure, so it wins over the gate and forces status=fail even where the reports
parse clean; write the cause you actually read rather than a category, since nothing parses it.
--fix-owner on every failure, tool and license failures included, since it is what fills
stage_specific.fix_owner. A fail_reason naming the guilty stage in prose while the flag was
omitted reads to the caller as "this stage could not tell", and brings a human in to re-derive
an answer you already had. A PPA gate compares a measured value against a target and either side
can be wrong, so before naming rtl-design, read <ppa>/ppa.json: a dim whose unit disagrees
with the number stored in it — an area_um2 target holding a NAND2 gate count, say — makes a
conforming design look over-budget, and no rebuild converges against it. Name specification
when the target is what is malformed, and omit the flag only when you have read both sides and
still cannot name an owner.
Exit 0 means written, pass or fail. Exit 2 is BLOCKED and never a status=fail: an empty
--fail-reason, or a program exception. stderr names which.
Return Contract
Emit STATUS: DONE as your last line once result.json exists, or
STATUS: BLOCKED <one-line reason> when nothing could be written. What runs next is the caller's
decision, taken from result.json.