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 test-tile      # the same at a 7 µm tile, as CI also runs it
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. CI runs the suite a second time with GDSCHECK_TILE_UM=7: no result may depend on the tile size, and a fixture whose shapes a 7 µm line happens to cross is where one did.

CI additionally runs the main suite over the real layouts in aesc-silicon/reference-designs (ci/run-designs.sh, one job per PDK) to catch crashes, hangs and false violations that the synthetic fixtures cannot. Run it locally with just designs <process> against a sibling checkout of that repository.

Every design’s run in that workflow prints its performance figures after the result (wall and CPU time, cores, peak memory, the slowest rules; ci/perf-summary.py), and the same land in the job summary as a table. For a number rather than a trend, compare two commits on this machine: ci/bench.sh <a> <b> [process...] builds both, runs every design under each in turns (RUNS times, THREADS threads, DESIGN=process:top:path for one more), and prints the best run per rule side by side, per check family and for the rules that moved. Runner times are noisy; CPU seconds, cores and peak memory are what to read there.

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 ring_covers_boundary 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 the antenna rule Ant.h’s derivation was once rewritten to avoid, before it became a chain of sentence layers in pdk.yml (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 (geom.rs’s facing-wall scan and checks/width/’s drivers, helper.rs’s region-spacing engine and 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.