Skip to main content

logicaffeine_compile/codegen_sva/
caravel_project.rs

1//! A Caravel user project as data.
2//!
3//! Caravel carries exactly one user macro, placed as `mprj` inside the fixed `user_project_wrapper`. Everything
4//! that differs between two user projects along the chain — macro harden, wrapper harden, golden-deck DRC,
5//! submission, `mpw_precheck` — is a field of [`CaravelProject`]. The flow scripts read their project through
6//! `emit_flow_config --caravel-project <key>` instead of naming a macro themselves, so a second user project is
7//! one more entry in [`CARAVEL_PROJECTS`], never a second copy of the scripts; and the locks walk the same list,
8//! so an entry is held to every gate from the moment it exists.
9
10use super::caravel_rtl::emit_user_project_wrapper_around;
11use super::flow_config::{MacroGrid, SynthStrategy};
12use super::pdk_target::{HarnessClock, HarnessTiming, PdkTarget};
13use super::signoff_blocks::registered_period_ns;
14use super::wpe_caravel::{WPE_CARAVEL, WPE_CLOCK};
15
16/// One user project on the Caravel harness.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct CaravelProject {
19    /// The name the flow scripts are invoked with.
20    pub key: &'static str,
21    /// The user macro's module. Its RTL is `benchmarks/silicon/<macro_module>.v`.
22    pub macro_module: &'static str,
23    /// What the macro carries, as the wrapper's header names it.
24    pub carries: &'static str,
25    /// The macro's die, and the lower-left corner the wrapper places it at, in µm of the user area.
26    pub macro_die_um: (u32, u32),
27    pub macro_origin_um: (u32, u32),
28    /// The harness clock the macro's logic runs on, and its period: the clock the chip runs the project at. On the
29    /// core clock the period is no shorter than the chip signs that clock off at; `user_clock2` takes its own.
30    pub clock: HarnessClock,
31    pub clock_period_ps: u32,
32    /// The macro's technology-mapping script, when OpenLane's default is not the one it closes with
33    /// (see `flow_config::ClosureSettings::synth_strategy`).
34    pub synth_strategy: Option<SynthStrategy>,
35    /// The hold slack every path of the macro is closed to (`flow_config::VehicleMacro::hold_margin_ns`), in ps.
36    pub macro_hold_margin_ps: u32,
37    /// The macro's global placement density target, in thousandths, when the closure default does not route it.
38    pub macro_density_permille: Option<u16>,
39    /// The macro's met1 global-route penalty, in thousandths, when the closure recipe's
40    /// (`flow_config::ClosureSettings::met1_route_penalty`) leaves it too little met1 to route.
41    pub macro_met1_penalty_permille: Option<u16>,
42    /// The macro's global-route overflow iterations, when OpenLane's own 50 do not route it
43    /// (`flow_config::ClosureSettings::grt_overflow_iters`).
44    pub macro_grt_overflow_iters: Option<u16>,
45    /// The share of each gcell's capacity the macro's global route holds back, in thousandths, when OpenLane's own
46    /// 0.3 leaves it over capacity (`flow_config::ClosureSettings::grt_adjustment`).
47    pub macro_grt_adjustment_permille: Option<u16>,
48    /// The share past each antenna ratio the macro's global router repairs to, in percent, when the closure default
49    /// (`flow_config::ClosureSettings::grt_antenna_margin_pct`) inserts more diodes than the route has room for.
50    pub macro_grt_antenna_margin_pct: Option<u8>,
51    /// How many passes the macro's global router makes to repair antennas, when the closure default
52    /// (`flow_config::ClosureSettings::grt_antenna_iters`) reroutes the block past what its route holds.
53    pub macro_grt_antenna_iters: Option<u8>,
54    /// The span, in µm, past which the macro's heuristic pass gives a net a protection diode before routing, when the
55    /// closure default (`flow_config::ClosureSettings::heuristic_antenna_threshold`) floods a dense patch with them.
56    pub macro_antenna_threshold_um: Option<u16>,
57    /// The sites a side the macro's legalization keeps around every cell, when abutting cells leave a dense patch's
58    /// pins no room (`flow_config::ClosureSettings::dpl_cell_padding_per_side`).
59    pub macro_dpl_padding_per_side: Option<u8>,
60    /// The setup slack, in ps, the macro's placement and global-route timing repairs close every path to, when its
61    /// routed paths at the max-RC corner outgrow the estimates the repairs judge
62    /// (`flow_config::ClosureSettings::setup_slack_margin_ns`).
63    pub macro_setup_margin_ps: Option<u32>,
64    /// How far below the slew limit the macro's post-diode upsize detects, in percent, at every corner
65    /// (`flow_config::ClosureSettings::post_diode_upsize_margin_pct`). The global-route estimate it detects on reads
66    /// below the extracted slew signoff judges, by an amount that depends on the design.
67    pub macro_upsize_margin_pct: u8,
68    /// How the macro takes power from the wrapper, and so how high it routes.
69    pub macro_grid: MacroGrid,
70    /// The OpenLane designs, under `tools/OpenLane/designs/`, the macro and the wrapper are hardened in.
71    pub macro_design: &'static str,
72    pub wrapper_design: &'static str,
73    /// The wrapper RTL under `benchmarks/silicon/`, staged into the wrapper design under the same name. Its module
74    /// is always `user_project_wrapper`, the harness's name for the user area.
75    pub wrapper_rtl: &'static str,
76    /// The ledger the macro's own block signoff is recorded in.
77    pub macro_ledger: &'static str,
78    /// The wrapper's signoff record, the key its golden-deck verdict is filed under in `golden_precheck.json`, the
79    /// precheck record, and the directory the submission is assembled in — regenerated OpenLane output, kept
80    /// beside the runs it is assembled from under the ignored `tools/`.
81    pub wrapper_ledger: &'static str,
82    pub golden_key: &'static str,
83    pub precheck_ledger: &'static str,
84    pub submission_dir: &'static str,
85    /// The submission files that are this project's own — README, pad configuration, bring-up bench — laid over
86    /// the efabless scaffold at the same relative paths.
87    pub overlay: &'static str,
88}
89
90/// The DRAM command-stream executor behind a Wishbone target: the project submitted and precheck-passed first.
91/// Every path is the one its evidence was recorded under.
92pub const DRAM_PROJECT: CaravelProject = CaravelProject {
93    key: "dram",
94    macro_module: "dram_user_project",
95    carries: "certified command player",
96    macro_die_um: (2800, 1760),
97    macro_origin_um: (60, 880),
98    clock: HarnessClock::Core,
99    clock_period_ps: 25_000,
100    synth_strategy: None,
101    macro_hold_margin_ps: 700,
102    macro_density_permille: None,
103    macro_met1_penalty_permille: None,
104    macro_grt_overflow_iters: None,
105    macro_grt_adjustment_permille: None,
106    macro_grt_antenna_margin_pct: None,
107    macro_grt_antenna_iters: None,
108    macro_antenna_threshold_um: None,
109    macro_dpl_padding_per_side: None,
110    macro_setup_margin_ps: None,
111    // The all-corner margin that closed `systolic_pipe16_realbist` and `systolic_os_pipe8_realbist`
112    // (RUN_2026.09.24_16.37.47 / _19.16.31), on which their slew violators were all slow-corner only.
113    macro_upsize_margin_pct: 20,
114    macro_grid: MacroGrid::Own,
115    macro_design: "logos_upm",
116    wrapper_design: "logos_upw",
117    wrapper_rtl: "user_project_wrapper.v",
118    macro_ledger: "benchmarks/silicon/caravel_macro_signoff.json",
119    wrapper_ledger: "benchmarks/silicon/caravel_signoff.json",
120    golden_key: "user_project_wrapper",
121    precheck_ledger: "benchmarks/silicon/mpw_precheck.json",
122    submission_dir: "tools/caravel_submissions/dram",
123    overlay: "benchmarks/silicon/caravel/dram",
124};
125
126/// The whole-program engine ([`wpe_caravel`](super::wpe_caravel)): its UART and its PSRAM on user GPIOs, clocked
127/// on `user_clock2` at the clock its UART bit period was derived from, faster than the chip signs its core clock
128/// for. Its ports are held to the harness's minimum input delays
129/// (`flow_config::vehicle_macro_sdc`), so its hold margin is OpenLane's placement default, 0.1 ns: the 0.7 ns the
130/// command player carries buffered 35,656 of the engine's paths.
131pub const WPE_PROJECT: CaravelProject = CaravelProject {
132    key: "wpe",
133    macro_module: "wpe_user_project",
134    carries: "whole-program engine",
135    macro_die_um: (2800, 3360),
136    macro_origin_um: (60, 80),
137    clock: WPE_CLOCK,
138    clock_period_ps: WPE_CARAVEL.clk_period_ps,
139    synth_strategy: Some(SynthStrategy::Area(3)),
140    macro_hold_margin_ps: 100,
141    // MEASURED: at the default 0.45 the placer packed 31% utilization into the center-right of the die and raised
142    // its own target to 0.51 for routability; global routing under the met3 ceiling then overflowed by 1,188 in a
143    // 1.6 x 1.2 mm patch with the die's left third empty (RUN_2026.09.26_01.14.22, GRT-0119). At 0.38 (raised to
144    // 0.416) it overflowed in 55 gcells beside 32,005 hold buffers (RUN_2026.09.26_03.38.30), and in 2 gcells,
145    // each by one track, around the FPU multiplier once the chip's signoff clock left 16
146    // (RUN_2026.09.26_05.24.57). Spreading further does not help: at 0.33 the wirelength grew 6% and 230 gcells
147    // overflowed (RUN_2026.09.26_07.55.00).
148    macro_density_permille: Some(380),
149    // MEASURED: the recipe's 0.7 leaves met1 20% of its tracks (785,461 of 3,936,600 after the rails), and with met2
150    // the only vertical layer under the met3 ceiling, the engine's global route came up two gcells short, both
151    // horizontal, by one track each (RUN_2026.09.26_05.24.57). The recipe's penalty closed one met1 side-area
152    // antenna violator on `gpu_simt`; this block's antenna stays with the diodes and the jog ECO. MEASURED at 18.5 ns,
153    // the route detailed routing follows (step 30) replayed from RUN_2026.10.24_03.18.57's step-29 database: at 0.5 it
154    // overflowed 248 gcells by a track in one 270 x 390 um patch over the integer multiplier, at 0.4 53, and at 0.4
155    // with the gcell adjustment at 0.05 none.
156    macro_met1_penalty_permille: Some(400),
157    // MEASURED at 16 ns in the private tree: at OpenLane's 0.3 adjustment the first global route (step 15) stopped at
158    // GRT-0119 at 50, 150 and 400 iterations alike, the iterations ending once the overflow could grow no further
159    // (RUN_2026.10.19_19.38.51, RUN_2026.10.19_20.34.27, RUN_2026.10.19_22.07.52); at 0.2 with 150 iterations it was
160    // clean (RUN_2026.10.20_17.57.25, x5 and x6).
161    macro_grt_overflow_iters: Some(150),
162    // MEASURED: at 0.2 the route detailed routing takes (step 25) overflowed in 72 gcells, by one track each over a
163    // ~19-track edge, all in one 400 x 190 um patch, once the fanout step had split 18,096 nets with 22,708 buffers and
164    // the timing repair had resized 7,575 cells (RUN_2026.10.20_17.57.25, x6, with the met1 penalty at 0.3 besides).
165    // 0.1 gives every layer an eighth more capacity than 0.2, against the twentieth that patch lacked. MEASURED at
166    // 18.5 ns on the same step-30 replay as the met1 penalty: at 0.1 the patch overflowed 248 gcells, at 0.05 83, and at
167    // 0.05 with met1 at 0.4 none.
168    macro_grt_adjustment_permille: Some(50),
169    // MEASURED at 18.5 ns on the registered cache-hit RTL, the global route replayed from RUN_2026.10.22_16.11.23's
170    // step-24 database: at the closure default of 50% the antenna repair's first pass found 1,412 violations and
171    // inserted 5,981 diodes, and its reroute of 11,725 nets stopped at GRT-0232; at 20% the first pass found 599 and
172    // inserted 1,537, and the next found 46 and 26. At either margin the repair stalls near twenty nets that each need
173    // more than ten diodes a gate, as at 48 ns, where the passes left 16 and 17 (RUN_2026.09.27_18.14.14,
174    // RUN_2026.09.28_20.20.58) and the block signed off antenna-clean.
175    macro_grt_antenna_margin_pct: Some(20),
176    // MEASURED on the same route at 20%: the passes found 599, 46, 26, 21, 16, 24 and 17 violations
177    // (RUN_2026.10.23_21.04.53), oscillating from the fourth on, as the earlier RTL's 18 passes at 50% oscillated
178    // between 27 and 44 (RUN_2026.10.21_09.00.47). Each pass reroutes the ~150 nets it touched, ever more slowly: the
179    // fourth took 100 minutes, the seventh had not ended after 14 attempts, and at 50% with a met1 adjustment of 0.4
180    // the thirteenth stopped at GRT-0232. Three passes route in 1 h 38 m and leave the same score of nets to the
181    // repair after detailed routing.
182    macro_grt_antenna_iters: Some(3),
183    // MEASURED at 18.5 ns: at the closure default of 300 um the heuristic pass inserted 57,637 diodes
184    // (RUN_2026.10.24_03.18.57), and the congested patch held 4,844 of its 14,681 cells as diodes, at 0.552 utilization
185    // against the core's 0.345. With both route levers the step-30 route then ran clean, but the slew rounds' buffers
186    // filled the patch until a reroute stopped at GRT-0232. The pass on the same placement inserts 39,821 at 400 um
187    // (2,536 of the patch's 3,798 before legalization), 28,338 at 500 and 12,226 at 700. All three then routed steps 25
188    // and 30, but detailed routing could not clear the met1 shorts the remaining diodes crowd in: 930 violations left
189    // after 21 passes at 400 um (RUN_2026.10.24_12.25.42) and 409, still falling by a few a pass, after 26 at 500
190    // (logos_wpe_upm_t500's RUN_2026.10.24_12.26.00), where 700 started its first pass with the fewest, 251,562
191    // against 265,814 and 273,753.
192    macro_antenna_threshold_um: Some(700),
193    // MEASURED at 18.5 ns, detailed routing replayed from RUN_2026.10.24_18.23.48's step-30 database (the recipe above)
194    // after legalizing it again: with the cells abutting, its met1 shorts stalled at 74 violations from the 38th pass
195    // to the 46th, where it was stopped; with a site kept free a side the passes left 591 against 909 at the 11th, 80
196    // against 179 at the 21st, 18 (10 met1 shorts, 5 met1 spacings, 3 on met2) from the 39th, 5 at the 50th, and none
197    // at the 56th; with two sites a side the patch over the multiplier filled, and the passes left 3,777 against one
198    // site's 2,171 at the 9th and stalled at 201 from the 22nd. The full flow with a site a side at every legalization
199    // (RUN_2026.10.25_06.23.41) held 78 from its 46th pass to its 51st, every one in a 170 x 90 um patch over the
200    // integer multiplier, met1 shorts at the pins of its nand2_1, a22o_1 and o21ai_1 gates.
201    macro_dpl_padding_per_side: Some(1),
202    // MEASURED at 18.5 ns on RUN_2026.10.25_06.23.41, every net routed (its detailed routing replayed to the 36th pass)
203    // and timed at ss_100C_1v60: the repairs had closed setup to +0.03 ns on their global-route estimates, and the
204    // routed design read -0.37 ns extracted at the nominal RC corner and -1.13 ns at the max-RC corner signoff judges,
205    // 1.16 ns past the estimate. At 21 ns, on RUN_2026.10.26_16.02.11, the repairs had nothing to do: the estimate
206    // read +0.757 ns before the fanout step's repair and after it.
207    macro_setup_margin_ps: Some(1250),
208    // MEASURED: at 20% (0.600 ns) the upsize converged to zero, yet net `_117618_` — 773 um, driven by an `a21o_4`,
209    // the top of its family — signed off at 0.757 ns at max RC (RUN_2026.09.27_18.14.14); a re-route of the same
210    // netlist estimated it at 0.635. Every pin of the engine signing off above 0.70 ns estimates at 0.56 ns or more,
211    // so 0.540 ns catches all of them with 0.05 ns for that route-to-route noise: 1,752 pins, where 0.600 caught 21.
212    macro_upsize_margin_pct: 28,
213    // MEASURED: with the penalty eased, met1 held 1,423,779 tracks and the misses moved to met2, the only vertical
214    // layer under the met3 ceiling: 59 of 89 overflowing gcells vertical (RUN_2026.09.26_09.04.50). Fed by the
215    // wrapper, the engine routes on met4 as well.
216    macro_grid: MacroGrid::WrapperFed,
217    macro_design: "logos_wpe_upm",
218    wrapper_design: "logos_wpe_upw",
219    wrapper_rtl: "wpe_user_project_wrapper.v",
220    macro_ledger: "benchmarks/silicon/caravel_macro_signoff.json",
221    wrapper_ledger: "benchmarks/silicon/caravel_signoff_wpe.json",
222    golden_key: "user_project_wrapper_wpe",
223    precheck_ledger: "benchmarks/silicon/mpw_precheck_wpe.json",
224    submission_dir: "tools/caravel_submissions/wpe",
225    overlay: "benchmarks/silicon/caravel/wpe",
226};
227
228pub const CARAVEL_PROJECTS: &[CaravelProject] = &[DRAM_PROJECT, WPE_PROJECT];
229
230/// Why a project cannot be hardened as described.
231#[derive(Debug, Clone, PartialEq)]
232pub enum CaravelProjectUnplaceable {
233    /// The process has no shuttle vehicle characterized here.
234    NoVehicle { pdk: &'static str },
235    /// The vehicle's harness timing — its input delays and the clock the chip is signed off with — has not been
236    /// read, so nothing on it can be timed.
237    VehicleUntimed { pdk: &'static str },
238    /// The macro, at its origin, does not lie inside the user area.
239    MacroOutsideUserArea {
240        corner_um: (u32, u32),
241        user_area_um: (f64, f64),
242    },
243    /// The macro is signed off at a period longer than the clock the chip runs at, so the chip's clock is a
244    /// frequency the macro does not guarantee.
245    MacroSlowerThanChip {
246        macro_period_ns: f64,
247        chip_period_ns: f64,
248    },
249    /// The project runs on the core clock faster than the chip signs that clock off at, so the management SoC and
250    /// the bus on the same clock would run at a frequency the chip does not guarantee.
251    CoreClockPastSignoff { period_ns: f64, signoff_ns: f64 },
252}
253
254impl std::fmt::Display for CaravelProjectUnplaceable {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            Self::NoVehicle { pdk } => write!(f, "{pdk} has no shuttle vehicle characterized (PdkTarget::user_area)"),
258            Self::VehicleUntimed { pdk } => {
259                write!(f, "{pdk}'s shuttle vehicle has no harness timing characterized (UserArea::harness)")
260            }
261            Self::MacroOutsideUserArea { corner_um, user_area_um } => write!(
262                f,
263                "the macro's far corner ({}, {}) um lies outside the {} x {} um user area",
264                corner_um.0, corner_um.1, user_area_um.0, user_area_um.1
265            ),
266            Self::MacroSlowerThanChip { macro_period_ns, chip_period_ns } => write!(
267                f,
268                "the macro is signed off at {macro_period_ns} ns but the chip runs at {chip_period_ns} ns"
269            ),
270            Self::CoreClockPastSignoff { period_ns, signoff_ns } => write!(
271                f,
272                "the project runs the core clock (wb_clk_i) at {period_ns} ns, which the chip signs off at {signoff_ns} ns; \
273                 a faster project runs on user_clock2"
274            ),
275        }
276    }
277}
278
279impl CaravelProject {
280    pub fn by_key(key: &str) -> Option<&'static CaravelProject> {
281        CARAVEL_PROJECTS.iter().find(|p| p.key == key)
282    }
283
284    pub fn clock_period_ns(&self) -> f64 {
285        self.clock_period_ps as f64 / 1000.0
286    }
287
288    /// The period the macro is hardened at: its registered signoff period once a run has measured one, and the
289    /// chip's clock for the build that takes the first measurement.
290    pub fn macro_period_ns(&self) -> f64 {
291        registered_period_ns(self.macro_module).unwrap_or_else(|| self.clock_period_ns())
292    }
293
294    /// The wrapper RTL: the harness's `user_project_wrapper` around this project's macro.
295    pub fn wrapper_verilog(&self) -> String {
296        emit_user_project_wrapper_around(self.macro_module, self.carries)
297    }
298
299    /// The project fits the process's vehicle, the vehicle's timing is known, the macro guarantees the chip's
300    /// clock, and a project on the core clock runs it no faster than the chip signs it off at.
301    pub fn check(&self, pdk: &PdkTarget) -> Result<(), CaravelProjectUnplaceable> {
302        let ua = pdk
303            .user_area
304            .ok_or(CaravelProjectUnplaceable::NoVehicle { pdk: pdk.name })?;
305        let harness = ua
306            .harness
307            .ok_or(CaravelProjectUnplaceable::VehicleUntimed { pdk: pdk.name })?;
308        let corner = (
309            self.macro_origin_um.0 + self.macro_die_um.0,
310            self.macro_origin_um.1 + self.macro_die_um.1,
311        );
312        if f64::from(corner.0) > ua.width_um || f64::from(corner.1) > ua.height_um {
313            return Err(CaravelProjectUnplaceable::MacroOutsideUserArea {
314                corner_um: corner,
315                user_area_um: (ua.width_um, ua.height_um),
316            });
317        }
318        if let Some(p) = registered_period_ns(self.macro_module) {
319            if p > self.clock_period_ns() {
320                return Err(CaravelProjectUnplaceable::MacroSlowerThanChip {
321                    macro_period_ns: p,
322                    chip_period_ns: self.clock_period_ns(),
323                });
324            }
325        }
326        if self.clock == HarnessClock::Core && self.clock_period_ns() < harness.core_clock_period_ns {
327            return Err(CaravelProjectUnplaceable::CoreClockPastSignoff {
328                period_ns: self.clock_period_ns(),
329                signoff_ns: harness.core_clock_period_ns,
330            });
331        }
332        Ok(())
333    }
334
335    /// The clocks the wrapper is timed at, in ns: `clk` on the core clock, at this project's period when it runs
336    /// there and at the chip's signoff period when it does not, and `usr_clk` on `user_clock2` at this project's
337    /// period when it runs there.
338    pub fn wrapper_clocks_ns(&self, harness: &HarnessTiming) -> (f64, Option<f64>) {
339        match self.clock {
340            HarnessClock::Core => (self.clock_period_ns(), None),
341            HarnessClock::User2 => (harness.core_clock_period_ns, Some(self.clock_period_ns())),
342        }
343    }
344
345    /// What the flow scripts `eval`: the project, then the clock the vehicle's chip is signed off with, which every
346    /// harden of the chain is timed at, then the clocks the wrapper is timed at ([`CaravelProject::wrapper_clocks_ns`],
347    /// `none` for a clock it does not have) — or the refusal, when the project does not fit the vehicle.
348    pub fn flow_shell(&self, pdk: &PdkTarget) -> Result<String, CaravelProjectUnplaceable> {
349        self.check(pdk)?;
350        let harness = pdk
351            .user_area
352            .and_then(|ua| ua.harness)
353            .ok_or(CaravelProjectUnplaceable::VehicleUntimed { pdk: pdk.name })?;
354        let (core, user2) = self.wrapper_clocks_ns(&harness);
355        Ok(format!(
356            "{}{}CW_CORE_PERIOD_NS={core}\nCW_USER_CLOCK2_PERIOD_NS={}\n",
357            self.shell(),
358            harness.shell(),
359            user2.map_or("none".to_string(), |u| format!("{u}"))
360        ))
361    }
362
363    /// The project as `NAME=value` lines for the flow scripts to `eval`. Every value is a bare word — an
364    /// identifier, a number or a repo-relative path — so the assignment needs no quoting.
365    pub fn shell(&self) -> String {
366        let vars: [(&str, String); 31] = [
367            ("CP_KEY", self.key.to_string()),
368            ("CP_MACRO", self.macro_module.to_string()),
369            ("CP_MACRO_DIE_W", self.macro_die_um.0.to_string()),
370            ("CP_MACRO_DIE_H", self.macro_die_um.1.to_string()),
371            ("CP_MACRO_X", self.macro_origin_um.0.to_string()),
372            ("CP_MACRO_Y", self.macro_origin_um.1.to_string()),
373            ("CP_CLOCK_PORT", self.clock.port().to_string()),
374            ("CP_CLOCK_PERIOD_NS", format!("{}", self.clock_period_ns())),
375            ("CP_MACRO_PERIOD_NS", format!("{}", self.macro_period_ns())),
376            (
377                "CP_SYNTH_STRATEGY",
378                self.synth_strategy
379                    .map_or("default".to_string(), |st| st.flag()),
380            ),
381            (
382                "CP_HOLD_MARGIN_NS",
383                format!("{}", self.macro_hold_margin_ps as f64 / 1000.0),
384            ),
385            (
386                "CP_DENSITY",
387                self.macro_density_permille
388                    .map_or("default".to_string(), |d| format!("{}", d as f64 / 1000.0)),
389            ),
390            (
391                "CP_MET1_PENALTY",
392                self.macro_met1_penalty_permille
393                    .map_or("default".to_string(), |p| format!("{}", p as f64 / 1000.0)),
394            ),
395            (
396                "CP_GRT_OVERFLOW_ITERS",
397                self.macro_grt_overflow_iters
398                    .map_or("default".to_string(), |n| n.to_string()),
399            ),
400            (
401                "CP_GRT_ADJUSTMENT",
402                self.macro_grt_adjustment_permille
403                    .map_or("default".to_string(), |a| format!("{}", a as f64 / 1000.0)),
404            ),
405            (
406                "CP_GRT_ANT_MARGIN",
407                self.macro_grt_antenna_margin_pct
408                    .map_or("default".to_string(), |m| m.to_string()),
409            ),
410            (
411                "CP_GRT_ANT_ITERS",
412                self.macro_grt_antenna_iters
413                    .map_or("default".to_string(), |n| n.to_string()),
414            ),
415            (
416                "CP_ANTENNA_THRESHOLD",
417                self.macro_antenna_threshold_um
418                    .map_or("default".to_string(), |um| um.to_string()),
419            ),
420            (
421                "CP_DPL_PADDING",
422                self.macro_dpl_padding_per_side
423                    .map_or("default".to_string(), |n| n.to_string()),
424            ),
425            (
426                "CP_SETUP_MARGIN",
427                self.macro_setup_margin_ps
428                    .map_or("default".to_string(), |ps| format!("{}", ps as f64 / 1000.0)),
429            ),
430            ("CP_UPSIZE_MARGIN", self.macro_upsize_margin_pct.to_string()),
431            ("CP_MACRO_GRID", self.macro_grid.flag().to_string()),
432            ("CP_MACRO_DESIGN", self.macro_design.to_string()),
433            ("CP_WRAPPER_DESIGN", self.wrapper_design.to_string()),
434            ("CP_WRAPPER_RTL", self.wrapper_rtl.to_string()),
435            ("CP_MACRO_LEDGER", self.macro_ledger.to_string()),
436            ("CP_WRAPPER_LEDGER", self.wrapper_ledger.to_string()),
437            ("CP_GOLDEN_KEY", self.golden_key.to_string()),
438            ("CP_PRECHECK_LEDGER", self.precheck_ledger.to_string()),
439            ("CP_SUBMISSION", self.submission_dir.to_string()),
440            ("CP_OVERLAY", self.overlay.to_string()),
441        ];
442        let mut s = String::new();
443        for (k, v) in vars {
444            assert!(
445                !v.is_empty()
446                    && v.chars()
447                        .all(|c| c.is_ascii_alphanumeric() || "_./-".contains(c)),
448                "{k}={v:?} is not a bare word"
449            );
450            s.push_str(&format!("{k}={v}\n"));
451        }
452        s
453    }
454}