Contributing

Development setup

Requires a recent Rust toolchain (edition 2024). The justfile collects the common developer tasks:

just build          # cargo build --release
just test           # cargo test --release
just clippy         # cargo clippy --release -- -D warnings
just gen-testdata   # regenerate the IHP SG13G2 test fixtures
just check          # clippy + test + cargo fmt --check — the pre-commit gate

Run just check before opening a change — it’s the same gate CI runs.

Adding a rule to a deck

Add an entry to the relevant pdks/<process>/decks/<name>.yml (see PDK authoring guide for the full rule format), then regenerate and extend the test fixtures (see Test fixtures and the generator below) rather than hand-crafting a GDS — the generator’s pattern helpers already cover the common shapes (min/max width, spacing, notch, enclosure, density) parametrically. Confirm the new rule with gdscheck show-deck before running it on anything real.

Adding a new check type

A check is a module under src/checks/ exposing a run function, dispatched by name from checks::run_rule (src/checks/mod.rs) on the rule’s check: string. Signature depends on what the check needs:

  • Pure geometry, no merge cache: fn run(rule, layout: &FlatLayout, dbu_to_um: f64) -> Vec<Violation>.

  • Needs the tiled merge cache: add merged: &mut MergedCache.

  • Needs per-run memoisation (e.g. an expensive whole-chip number several rules share): add cache: &mut Cache.

  • Net-aware: add conn: Option<&Connectivity> and list the check’s name in NET_AWARE_CHECKS (lib.rs) so connectivity is built lazily when the deck needs it, and skipped cleanly under --no-connectivity.

Prefer the tiled merge cache over a global merge unless every layer the check reads is genuinely sparse (isolated vias, contacts, device markers — see must_interact for a deliberate, documented example of when a global merge is safe). A check that globally unions a dense, chip-wide layer works on a small test pattern and then runs out of memory the first time someone points it at a real SoC — exactly the failure class forbidden_unless_labeled was rewritten to avoid (see Architecture, Region stitching and Lazy virtual layers, for the primitives available: MergedCache::regions/stitch_labeled for whole-region area/marker/ predicate aggregation, register_virtual for a derivation chain expressed as tiled virtual layers).

Register the new check name in checks::run_rule’s match, write its reference page (the six-section template every existing page follows — copy the closest existing check’s structure), and add it to docs/source/checks/index.rst’s toctree.

Test fixtures and the generator

gen/ is a separate binary (gen-testdata) that writes synthetic pass/fail GDS fixtures into tests/data/<pdk>/<deck>/ for every rule, using parametric pattern helpers in gen/helpers.rs (min_width_pattern, space_pattern, notch_pattern, density_pattern, enclosure_pattern, …) — read the existing helper doc comments before writing a new one; most new fixture needs are a variation on an existing pattern’s parameters, not a genuinely new shape.

The convention (see any existing gen/ihp_sg13g2/*.rs generator function): one function per rule id, writing a small number of shapes designed to sit exactly on the pass/fail boundary (a clean case at exactly the limit, a failing case a half-grid-unit past it) — not realistic-looking layout, the smallest shape that unambiguously exercises the rule. tests/ihp-sg13g2.rs then asserts the exact set of rule ids a fixture triggers (via an ignore list for expected incidental violations from unrelated rules on the same shapes), so both false negatives and false positives are caught.

When you add or change a generator function, run just gen-testdata to regenerate the affected fixtures, then just test — always verify the actual violation list against the real binary (gdscheck run --deck <name> --input <fixture> --verbose) before encoding an assertion, rather than assuming a fixture produces what you intended.

Validating against the reference KLayout deck

For a rule with a KLayout reference implementation (IHP publishes .lydrc scripts for SG13G2), cross-check a real design with both engines before trusting a new/changed check. Reading the reference script directly — not just its rule-name description — matters: the same DRC-DSL operator (e.g. Region#space) can mean something subtly different than the obvious gdscheck equivalent (see min_notch’s KLayout-equivalent section for a worked example, where KLayout’s single space operator folds together what gdscheck reports as two separate checks).

When counts genuinely differ, verify by pulling the real geometry at the reported coordinates (a small KLayout Ruby/Python script querying the actual GDS) rather than trusting either tool’s report blindly — several real discrepancies this project has hit turned out to be one tool reporting a genuine duplicate (KLayout’s two-pass angle-metric scan reporting the same notch twice) rather than the other tool missing something.

Parity testing between PDKs

A derived PDK (extends:, see PDK authoring guide) inherits its base’s layer and virtual-layer tables but declares its own decks — there’s no automatic guarantee a rule that exists in both processes stays behaviourally identical as either evolves. When changing a check or a shared virtual layer, re-run the affected deck against both PDKs’ test fixtures (cargo test already covers this if fixtures exist for both; see tests/ihp-sg13cmos5l.rs alongside tests/ihp-sg13g2.rs) rather than assuming a fix verified against one PDK holds for the other.

Coding conventions

  • Follow the message convention in violation.rs’s module doc for any new Violation: name the layer(s), state <measured> <cmp> <limit> with units, end with the location — consistency here is what makes report diffing and KLayout cross-checks tractable.

  • Prefer extending an existing shared primitive (helper.rs’s facing-wall scan, region-spacing engine, or boolean-residual/extension engines; merge.rs’s tiled virtual-layer and region-stitching machinery) over writing a new one-off algorithm — most new checks are a variation on an existing measurement, not a new kind of geometry problem.

  • Comments explain why, not what — a hidden constraint, a subtle invariant, a workaround for a specific upstream quirk. Well-named code and the check’s own reference page should already answer what.

  • Run just check (clippy + tests + cargo fmt --check) before considering a change done.