Repository navigation
v0.2.0
0.2.0 (2026-08-04)
The first release since 0.1.0. klt grew from 5 verbs to 24; the 19
net-new verbs below all existed on main but had never shipped in a
numbered release, so anyone installing from PyPI has until now been getting
a tool bearing little resemblance to the documented one.
Additive at the CLI surface — no existing verb was renamed or removed. One
per-command breaking change is included (klt precheck's own
schema_version moved 1 -> 2, detailed below); per
docs/json-contract.md that is scoped to that
command and does not affect any other verb.
Added — new verbs
klt precheck— pre-flight layout checks before an expensive runklt socket-check— socket/pin-level connectivity checksklt ring-check— guard/collector ring integrity checksklt layout-metrics— quantitative layout measurementsklt render— raster/vector rendering of layout viewsklt extract— parasitic/device extraction with provenance hashingklt lvs— layout-versus-schematic comparisonklt gen— parametric analog cell generationklt gen-compose— composition and routing of generated blocksklt draw— primitive layout drawingklt sim— circuit simulation, including the remote AWS backendklt report— aggregated run reportingklt kb— knowledge-base query/updateklt trajectory— append-only optimization-run logs (--plot)klt synthesize— RTL synthesis (Yosys engine class)klt place-and-route— physical implementation (OpenROAD engine class)klt functional-verification— cocotb testbenches via Icarus/Verilatorklt eval— singlevalid/objective/metricsverdict over gate checksklt lef-abstract— LEF abstract generation
Each is documented in docs/cli/.
Changed — behavior detail
The user-visible, additive behavior changes worth calling out explicitly,
because they affect a verb's output. Not an exhaustive commit-by-commit log.
- 2026-08-04 —
klt sim: wall-clock budget, orphan safety, and resume for
corner sweeps (#482).options.wall_clock_budget_s/--budget-sbounds
the overall sweep (distinct from the existing per-corner
options.timeout_s); an always-on parent-PID liveness check stops the
local/local-parallel dispatch loop the instant the launching process
exits; andoptions.resume/--resumecheckpoints completed corners to
checkpoint.jsonand skips them on a later matching invocation. Skipped
corners are reported asstatus: "error"with
budget_exceeded/orphaneddiagnostics, andenvironment.budget/
environment.orphaned/environment.resumesummarise the outcome —
purely additive. - 2026-08-04 —
klt extract: comma-joined multi-label net collision
detection (#481, closes #470). Any net whose KLayout-assigned name is a
comma-joined merge of 2+ distinct labels (e.g.Y,Y2) — a silent signal
that two differently-named nets were shorted together in the layout, most
commonly agen-composepins[]entry naming a port other connectivity
already reaches — is now flagged in a new top-level
merged_net_labels[]array ({"net", "labels"}per affected net) plus a
matchingwarnings[]entry. Purely additive:
net_count/nets[]/pin_countare unchanged. - 2026-08-04 —
klt gen-compose: self-net short detection now reads drawn
pad geometry, not just port adjacency (#472, closes #469).
route_two_pin()gains a fourth self-net check that intersects the
route's own drawn metal against the block's actual drawn shapes on the
route layer, generalising #467's same-row/same-direction heuristic
(still kept) to same-facing pin pairs on different rows and wider routes
that reach an adjacent row's pad — cases missed because a port's
reportedwidth_umsystematically under-states the pad metal drawn
around it. - 2026-08-04 —
klt gen:res_arraycan draw sky130's higher-sheet-rho
precision-resistor flavours (#475, closes #463). A validatedflavor
request param ("generic"/"high"/"xhigh", mirroringmos_array's enum
pattern) resolves a per-family implant/block layer pair sourced from the
extraction deck's ownrequireslayers (sky130res_high_po= psdm +
rpm;res_xhigh_po= psdm + urpm), so the generated array extracts as
the requested device class instead of alwaysres_generic_po. Default
"generic" reproduces prior geometry exactly on both families; an
unsupported flavour raises a clearGenError. - 2026-08-04 —
klt gen: MOS generators draw a poly gate landing pad past
the diffusion so gate contacts land legally (#474).mos_arrayand
diff_pairpreviously drew gate poly sharing the diffusion's exact
extent, leaving no DRC-legal spot for a gate contact. The shared unit
layout now adds a landing pad extending past the diffusion's gate-side
edge on the first finger and reports the gate port at the pad centre —
a JSON-contract-visible change: the gate port'sy_ummoves into the
extension and itswidth_umbecomes the pad width, and
bbox_height_umnow spans diffusion plus pad whileheight_umstays
the bare diffusion height. A contact placed at the reported gate port is
now DRC-clean on the curated sky130 deck. - 2026-08-04 —
klt extract:dummymarker-layer suppression (#295)
extended from MOS gates to resistors and bipolars (#471, closes #462).
Thedummyregion is now subtracted from each candidate resistor body
before device recognition and frombipolar_basebefore
emitter/collector are derived, with fully-covered components counted
into the shareddummy_devices_dropped— so matched resistor/bipolar
arrays built with dummy edge units no longer inflatedevice.unmatched
underklt lvs, and a fully dummy-suppressed poly resistor no longer
trips a falsemarked_unrecognisedwarning. - 2026-08-04 —
klt lvs: newdevice.combine_incompletemismatch category
(#466). Only possible withoptions.combine_devices: true. KLayout's own
klayout.db.Netlist.combine_devices()can raise an unhandled
internal-consistencyRuntimeErroron a partial-match device group — N
real (matching-relevant) instances plus M dummy instances that all share
two of three terminals (e.g. a bipolar device's base and collector, tied
to a matched array's common well and substrate), but only the N real
instances additionally share the third (e.g. an emitter bussed to one
signal net).klt lvsnow catches that one error shape per netlist
(narrowly — only aRuntimeErrorcarrying KLayout's own"...in Netlist.combine_devices"marker text; any otherRuntimeErrorstill
propagates as an application error) instead of letting it abort the whole
run: whatevercombine_devices()already merged stays merged, that
netlist's remaining devices are left as individual devices, and a
severity: "warning"entry (never changesstatus, never breaks
mismatch_count's error semantics) records that combine did not fully
apply on that side. Purely additive (noschema_versionbump), but adds a
category_counts["device.combine_incomplete"]entry for any
combine_devicesrun that trips the KLayout error — see
docs/cli/lvs.md'sdevice.combine_incompletesubsection for the full
trigger conditions. - 2026-08-04 —
klt gen+klt gen-compose: ring routing openings (#434).
guard_ring,diff_pair(add_guard_ring) andbjt_array
(add_collector_ring) acceptring_gap_side(""/"N"/"S"/"E"/
"W"),ring_gap_umandring_gap_offset_um, cutting one opening
through the ring's band — on every layer the ring is drawn on, dropping
any contact it would clip — so the ring stays a single connected C-shaped
conductor rather than splitting into two arcs. The opening is reported as
a newGAP_<side>entry inports[](width_um= the opening's length
along that side,direction_deg= the side's outward normal), and
klt gen-composeuses it to admit a route to a ringed block's non-tap
port — but only when the drawn backbone actually passes through the
opening with half the route width plus the block's own
drc_hints.min_spacing_umof clearance from either cut end; a crossing on
any other side, a crossing that misses the opening, and a segment laid
along a ring side are all still reported inunrouted_nets[]. With no
opening declared, the #199 closed-ring rejection is unchanged (its message
now also names the new remedy). Wiring or labelling aGAP_*port is an
application error (exit1). Before this, a matched analog group could
keep its default guard/collector ring or be wired into the rest of a
composed circuit, not both. Seedocs/cli/gen.md("Ring routing
openings") anddocs/cli/gen-compose.md. - 2026-08-03 — Breaking (per-command
schema_versionbump 1 -> 2):klt precheck:layer_whitelistviolations'shapescount is now weighted by
placement multiplicity across the full cell hierarchy, not summed once per
cell definition (issue #452). Previously,layout.each_cell()counted
each cell definition's own (non-recursive) shapes exactly once regardless
of how many times that cell was actually placed, under-reporting true
placed-shape prevalence by roughly two orders of magnitude on real
hierarchical, macro-scale input — e.g. a layer drawn 800 times across 320
placedsky130_fd_sc_hdinstances reported"shapes": 10. The fix
multiplies each cell definition's own-shape count by its total placement
multiplicity across all instantiation paths (multiplicities compose
multiplicatively for nested placement — a cell placed inside a cell that
is itself placed multiple times gets the product, not the sum), via a
top-down hierarchy walk (Layout.each_cell_top_down()/Instance.size())
rather than a full recursive shape flatten. Onlyklt precheck's own
schema_versionmoved to2; every other command'sschema_versionis
unaffected, and a per-command bump never forces one on the package version
— per-command versioning perdocs/json-contract.md. Seedocs/cli/precheck.md. - 2026-08-03 —
klt eval:synthesizeandplace-and-routeare now
first-class gatechecks (#437, Phase 5 of Epic #391), joining
drc/lvs/sim/layout-metrics/functional-verification. Both always
report"status": "ok"(seedocs/cli/synthesize.md/
docs/cli/place-and-route.md— a run either produces its output or
raises), so — likelayout-metrics— a gate naming either one must
declare an explicitthresholdto derive pass/fail;requestresolves
as a path only (never inline JSON/-), matching each verb's own
load_request. This is what lets a digital candidate's descriptor chain
synthesize->functional-verification->place-and-route->
drc/layout-metrics(over the P&R-produced GDS) into the same single
valid/objective/metricsverdict an analog descriptor already
produces, and record it as oneklt trajectorylog entry via the same
gates/objectiveshape. No schema change toklt eval's own response
envelope. Seedocs/cli/eval.md. - 2026-08-03 —
klt trajectory+klt eval: a scored evaluation can now be
recorded to a trajectory log without hand-rolling the record (#437, Phase 5
of Epic #391 — the trajectory-log half of that issue, completing the gate
wiring above). Newklayout_tools.trajectory.record_from_eval/
append_recordbuild one trajectory record straight from aklt eval
envelope's ownobjective/gates(collapsinggates[]to the record
schema's lightergate_results) and append it to an append-only JSONL log,
creating the file and any missing parent directories on first write. Both
are check-name-agnostic — they read onlyobjective/gates, so an analog
envelope (drc/lvs/sim/layout-metrics) and a digital one
(synthesize/functional-verification/place-and-route) log identically.
klt evalgains--trajectory-log/--turn/--candidate-ref/
--description/--wall-clock-s, which call that pair as a side effect on
top of the unchanged response envelope — so one invocation can score a
candidate and log the turn, matchingklt trajectory --plot's existing
"write a file as a courtesy, independent of--format" precedent.
--trajectory-logwithout both--turnand--candidate-ref, and a
log-append failure, are both application errors (exit1). See
docs/cli/eval.md's "Trajectory logging" section and
docs/cli/trajectory.md's "Building a record fromklt eval" section. - 2026-08-03 —
klt functional-verification: new verb (#422, Phase 3 of
Epic #391). Runs a cocotb testbench against RTL sources through Icarus
Verilog (default) or Verilator, reportingstatus
("pass"/"fail"),test_count/passed_count/failed_count/
skipped_count, a per-testtests[]array (witherror_type/
error_messageon failures), optional Verilatorcoverage
(line_pct/toggle_pct/branch_pct/expr_pctplus an lcovinfo_path),
and anenvironmentreproducibility block. Invoked exclusively through
cocotb 2.0's first-party PythonRunnerAPI — never a generated
Makefile — and the verdict is always derived from the run's own
results.xml, never from a simulator's exit code (which the Phase 1
survey observed varying between0,1, and2for the same failing
regression). Exit codes reuseklt lvs's0/1/2/3trichotomy, so
status: "pass"→valid: trueandstatus: "fail"→valid: falseat
theklt evalboundary;functional-verificationis also now a
first-classklt evalgatecheck. cocotb is an optional runtime
dependency (not pinned inpyproject.toml— cocotb 2.0 caps at Python
3.13 whilekltsupports 3.10+). See
docs/cli/functional-verification.mdand
docs/design/cocotb-verification-spike.mdsection 7 for the full
contract. - 2026-08-03 —
klt functional-verification:options.random_seed(issue
#423). Pinned toRunner.test()'s ownseedparameter
(COCOTB_RANDOM_SEED) when given; the effective seed cocotb actually used
(pinned or its own generated value) is always echoed back in
environment.random_seed, read fromresults.xml's own
<property name="random_seed">— the same reproducibility barklt sim's
Monte Carlo seeding andklt lvs'senvironmenthashes already set. CI
now also provisions pinned, checksum-verified Icarus Verilog/Verilator
builds and the pinned cocotb extra (scripts/install-icarus-verilog.sh,
scripts/install-verilator.sh,pyproject.toml's
functional-verificationextra), so the GCD worked example
(docs/design/cocotb-verification-spike.mdsection 6) runs for real in CI
against bothengine: "icarus"andengine: "verilator"rather than only
locally. - 2026-08-03 —
klt place-and-route: new verb (#425, Phase 4 of Epic #391).
Places and routes a gate-level netlist (klt synthesize's own
netlist_pathoutput) against a resolved sky130 standard-cell LEF/liberty
deck via OpenROAD's native Tcl API, one subprocess per stage
(floorplan->place->cts->route), chained viawrite_db/
read_dbODB checkpoints, with-metrics <file>.jsonas the structured
per-stage metrics channel (confirmed end-to-end against a real
openroad/orfsrun for this issue's own worked example). Supports all
three non-padframe floorplan methods (utilization/explicit/def);
target_stagemakes a partial run (e.g."place") a normal, successful
outcome withdef_path/gds_pathbothnullby design.def_pathis
populated oncewrite_defhas run;gds_pathonly once the DEF is
merged with the resolved standard-cell GDS view via KLayout'spya, in
-process (never aklayoutsubprocess) — ported from ORFS's own
def2stream.py. Adds a LEF resolver (klayout_tools.pdk.lef_files())
alongside the existing liberty resolution.seedis a required request
field, echoed unchanged in the response. Seedocs/cli/place-and-route.md
anddocs/design/digital-flow-contracts-spike.mdsection 5 for the full
contract. - 2026-08-03 —
klt synthesize: new verb (#416, Phase 2 of Epic #391).
Synthesizes RTL sources against a resolved sky130 standard-cell liberty
via Yosys + bundled ABC (read_verilog->hierarchy->synth->
dfflibmap->abc -liberty->clean->stat/write_verilog,
generated into a debuggable.ysscript kept alongside the mapped
netlist), reportinginstance_count/area_um2/sequential_area_um2/
instance_counts_by_typeparsed from Yosys's ownstat -liberty ... -jsonoutput.pdk.cell_library/cornerresolve to a liberty file via
the samefind_pdk()/libs_refdiscoveryklt pdk/klt cellsalready
use — no new PDK-fetch mechanism.timingis alwaysnullin this
contract, deferred to a future OpenROAD/OpenSTA place-and-route phase.
Seedocs/cli/synthesize.mdand
docs/design/digital-flow-contracts-spike.mdsection 4 for the full
contract. - 2026-08-03 —
klt trajectory: new verb (#388). Renders an append-only
JSONL optimization-trajectory log (one record per evaluation:turn,
candidate_ref,objective{name, value, polarity}, optional
gate_results/wall_clock_s) into a markdown milestone table plus a
self-contained objective-vs-turn SVG plot for a block repo's README.
Milestones are the turns where the objective improves on the best-prior
record by more than a configurable--threshold. Operates purely on the
JSONL file — no live optimizer required — so a hand-written or
human-curated log renders identically. The record schema mirrors the
plannedklt evalenvelope'sobjective/gate_resultsshape (#387). See
docs/cli/trajectory.md. - 2026-08-02 —
klt lvs: newdevice.body_unverifiedmismatch category
(a483ed0, #281/#285). Warns (severity: "warning", never changes
status) when a MOS body terminal was extracted onto a deck-synthesized
net rather than a real drawn tap/well-label net — an NMOS entry fires on
every inline-extraction LVS run with one or more NMOS devices (no curated
deck draws a distinct NMOS substrate/tap layer), and a PMOS entry
additionally fires for decks with no distinct well-tap layer (gf180mcu
today). This is purely additive (noschema_versionbump) but changes
category_countsfor any gf180mcu (and, for the NMOS case, sky130)
inline-extraction fixture that previously reported an empty
category_counts: {}— seedocs/cli/lvs.md'sdevice.body_unverified
subsection for the full trigger conditions. - 2026-08-02 —
klt lvs: new top-levelnet_correspondence[]response
field (#311). Lists every layout↔reference net pairing the comparer
matched — unambiguous and ambiguously-resolved alike — as{layout, reference, pin}entries, sorted and deduplicated per circuit scope so
len(net_correspondence) == counts.nets.matchedholds even across a
hierarchy with cross-circuit net-name collisions. Purely additive (no
schema_versionbump) — seedocs/cli/lvs.md'snet_correspondence[] entriessubsection. - 2026-08-02 —
klt extract: the "unmodelled device geometry" diagnostic
(#288/#299) no longer flags a recognised drawn resistor's own terminal
head (#324) — a poly component abutting a body region_resolve_resistors
already recognised is now excluded outright, the same way a real MOS gate
already was, removing a false positive that previously fired on any
resistor whose wide terminal head carries an ordinary (2+) contact array.
New top-levelunmodelled_poly[]response field lists the bounding
box +reason("unmarked"/"marked_unrecognised") of every shape the
diagnostic still flags, alongside the existing prosewarnings[]strings.
Purely additive (noschema_versionbump) — seedocs/cli/extract.md's
"Known limitation: unmodelled device geometry" subsection. Ordinary poly
routing tracks sharing the same resistor-body signature remain a known,
documented false-positive class with a client-side filtering workaround
viaunmodelled_poly[]. - 2026-08-02 —
klt sim: new optional request blockmonte_carlo(#348,
phase 1 of #344's decomposition) — re-runs each expanded corner point
ntimes with a reproducible seed (monte_carlo.seed), standing in for
the per-instance device variation a mismatch-aware model library's
behavioral parameters draw on. Addsenvironment.monte_carlo(echoes
n/seed/vary) and a per-samplecorners[].monte_carlofield
({sample_index, seed, process_seed, mismatch_seed},nullfor a
non-sampled corner); a sample'scorner_id/artifact path gets a
/mc<sample_index>suffix so per-sample logs never collide. Ships the
deterministic negative control two public canary repos already rely on
in their own hand-rolled orchestration: the seed component for whichever
axismonte_carlo.varydoes not request stays identical across every
sample of a corner (sigma=0), proving the sampler isn't silently
injecting or dropping variation. Purely additive (noschema_version
bump), reuses the existinglocal/local-parallel/remotebackends
unchanged — seedocs/cli/sim.md's "Monte Carlo sampling" section.
Statistics rollup and limit-window evaluation across a sample set landed
separately as phase 2 (#349, below). - 2026-08-02 —
klt lvs: new acceptedrequest.enginevalue"netgen"
(#343) — a second, independent comparator behind the same request/response
contract, wrapping the open-flow standard
netgenas a subprocess
(netgen -batch lvs). Netlist-vs-netlist only — nomagicextraction
backend; it compares the same layout/reference SPICE netlists the
"klayout"engine already resolves, so it validates comparator/contract
independence, not extraction independence (seedocs/cli/lvs.md→
"Engine"). Adds a newmismatches[].detailsfield (object |null) for
engine-specific data that does not map onto
category/net/device/property— present andnull-valued on every
"klayout"-engine entry too, so no entry shape changed — plus two
netgen-only request options (options.netgen_setup,
options.netgen_timeout_s).environment.engine_versionis netgen's own
banner-reported version for this engine. Known, documented gap:
counts.*.matchedis exact on a"match"verdict and0on a
"mismatch"verdict, andnet_correspondenceis always[], for
"engine": "netgen"only. Purely additive (noschema_versionbump); the
default engine is still"klayout"and its output is unchanged apart from
the newnulldetailskey. Findings (netgen invocation quirks, report-
format stability) are written up in
docs/design/lvs-extraction-spike.md's 2026-08-02 addendum. - 2026-08-02 —
klt sim: Monte Carlo statistics rollup (#349, phase 2 of
#344's decomposition). A measurement that ran undermonte_carlonow
carries an additivemeasurements[].monte_carloblock —
{n, errored, mean, stddev, min, max, quantiles, sigma_window, by_corner}— so callers no longer reduce the raw per-sample corner list
themselves.stddevis the sample (n-1) standard deviation and isnull
forn < 2rather than a fabricated0.0;quantilesdefaults to
[5, 50, 95]and is configurable via the newmonte_carlo.quantiles
request field. The newmonte_carlo.k_sigma(overridable per measurement
withmeasurements[].k_sigma) opts into amean ± k*stddev
limit-window check; both endpoints are scored through the existing
_evaluate_limits, so min/max handling and the margin sign convention
are shared with the path a single deterministic value takes —
a failing window makes the runfail(exit3) even when every
individual sample passed its limits. Without a declaredk_sigma,
pass/fail behavior is unchanged; withoutmonte_carlo, the response
shape is unchanged (noschema_versionbump) — seedocs/cli/sim.md's
"Monte Carlo statistics" section. - 2026-08-02 —
klt sim: newenvironment.monte_carlo.family_mismatch[]
response field (85faf9d, #365) — per-device-family mismatch-section
availability for the selected model library, so a Monte Carlo consumer
can detect which families actually sampled variation. Purely additive
(noschema_versionbump), but changesenvironmentcontents for every
gf180mcu Monte Carlo run — seedocs/cli/sim.md's
environment.monte_carlosubsection. - 2026-08-03 —
klt lef-abstract: new verb (#438, Epic #393 Phase 2
Capability A). Emits a LEF abstract (MACROblock withPIN/OBS
sections) from a block's GDSII/OASIS layout plus itsklt socket-check
descriptor, so OpenROAD can place it as a hard macro. Pin geometry is
real drawn metal when present, else a synthesized placeholder box
(reported per pin asgeometry_source); obstruction geometry is every
routing-layer shape not already claimed by a declared pin. Backed by
klayout_tools.lef_header, a new dependency-free tech-LEF header reader
(SITE, routing-layerPITCH/OFFSET/DIRECTION, macroPIN
DIRECTION/USE) — the deferred-trigger resolution
docs/design/sc-leflib-evaluation.mdcalled out.docs/schemas/socket .schema.jsongains two new optional, additivepins[]fields
(direction/use, LEF's own vocabulary) this command consumes when
present. Seedocs/cli/lef-abstract.mdand
docs/cli/socket-check.md's new "LEF translation" section. - 2026-08-03 —
klt place-and-route: newrequest.macrosfield (#438),
closing the "macro placement" gap the verb's own v1 docstring originally
scoped out. Each entry fixes one hard-macro instance (e.g. aklt lef-abstractLEF) at a caller-given location during the floorplan stage
via OpenROAD's ownplace_macro -location ... -orientation ... -exact
(never the automatic macro placer). The DEF→GDS merge tolerates an
abstract-only macro instance (no declaredgds) staying empty instead of
raising; a declaredgdsmerges the macro's own view in, same as the
standard-cell GDS merge. Purely additive ([]/omitted unchanged; no
schema_versionbump) — seedocs/cli/place-and-route.md's new
"Hard-macro placement" section. - 2026-08-04 — Epic #393 Phase 3 (cross-domain signoff, #456):
klt drc,
klt lvs, andklt extractverified against a real mixed
sky130_fd_sc_hd + analog-macro layout (aklt gen diff_pairblock placed
viaklt place-and-route'srequest.macrosalongside real standard
cells, real Yosys + real OpenROAD). No code changes were needed in any of
the three verbs — each already spans both domains by construction (klt drc/klt extract's whole-layout flattening has no region concept to
scope by;klt lvs's comparator has no macro-boundary special case).
Findings and the injected-violation/corrupted-reference verification
methodology are recorded indocs/cli/drc.md,docs/cli/lvs.md, and
docs/cli/extract.md's new "Mixed sky130_fd_sc_hd + analog-macro
layout"/"...netlist" sections. One real domain-boundary gap was found and
filed separately (not fixed here, per this phase's own decomposition
precedent, #451/#452): #464, aklt lef-abstractmacro pin with no
PORTgeometry that, if wired into a real net, failsklt place-and-routeopaquely (OpenROADGRT-0029) rather than with a clear
klt-level error. This is a documentation/verification-only change — no
schema_versionbump for any of the three verbs. - 2026-08-04 — Fixed #464 (per the #451/#452 precedent of fixing rather
than only documenting a verb-boundary gap).klt lef-abstractgains a
new top-levelunroutable_pinsresponse field ([{name, layer: [gds_layer, gds_datatype]}, ...]) that promotes the existing per-pin
geometry_source: "none"/warnings[]signal into a structured,
programmatically-checkable echo — purely additive,[]when every pin
resolved a routing-type LEF layer, noschema_versionbump.klt place-and-route's_validate_macrosnow cross-checks each macro's LEF
PINblocks (via a newhas_portfield onklayout_tools.lef_header's
parsedpins[]) against the netlist's own named port connections for
that instance, and rejects the request (exit 1, before OpenROAD is ever
invoked) when aPORT-less pin is actually wired to a real net — turning
the opaqueGRT-0029global-routing failure into a specific, actionable
klt-level error. APORT-less pin the netlist leaves unconnected is
not rejected; the netlist scan is a best-effort structural-Verilog lookup
(paren-depth-matched, comment-stripped) that is skipped — never a
spurious reject — when the macro's own instance can't be confidently
located in the netlist text. Seedocs/cli/lef-abstract.md's "Pins" and
docs/cli/place-and-route.md's "Hard-macro placement" sections.