Skip to main content

rhdl_gen/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::{
5    collections::BTreeMap,
6    ffi::OsString,
7    fmt::Write as _,
8    fs::{self, OpenOptions},
9    io::Write as _,
10    path::{Path, PathBuf},
11    process::Command as ProcessCommand,
12    sync::atomic::{AtomicU64, Ordering},
13    time::{Duration, Instant},
14};
15
16use anyhow::{Context, Result, anyhow, bail, ensure};
17use serde::{Deserialize, Serialize};
18use sha2::{Digest as _, Sha256};
19
20mod benchmark_kernel;
21mod full_sha_check;
22mod full_signer_check;
23mod signer_throughput_check;
24mod u280_hbm;
25mod u280_hbm_check;
26mod u280_hbm_rtl_check;
27
28/// `HashSigsRS` revision used by the compatibility profile.
29pub const HASHSIGS_REVISION: &str = "2d315dd4168804b7cbc51c51a1bf7ca27bf74140";
30
31/// RHDL revision used to lower these Rust kernels to Verilog.
32pub const RHDL_REVISION: &str = "c99d5cc53269a247bbc675d0fbd766991d409f56";
33
34/// Evidence tier written to every generated artifact manifest.
35pub const GENERATED_VERILOG_EVIDENCE_TIER: &str = "generated-verilog";
36
37/// Command-line usage text.
38pub const USAGE: &str = "Usage:\n  rhdl-gen list\n  rhdl-gen generate <TARGET> --output <DIRECTORY> [--force]\n  rhdl-gen check [--full] [--signer-full] [--temp-root <DIRECTORY>] [--keep]\n  rhdl-gen signer-throughput --source-evidence <DIRECTORY> --output <NEW_DIRECTORY>\n  rhdl-gen benchmark-kernel --output <NEW_DIRECTORY>\n  rhdl-gen u280-link-graph --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --output <NEW_JSON>\n  rhdl-gen u280-verify-link-graph --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --record <JSON>\n  rhdl-gen u280-hbm-rtl-check --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --record <JSON> --output <NEW_DIRECTORY>";
39
40/// Parsed command-line operation.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum Command {
43    /// List all registered generation targets.
44    List,
45    /// Generate one target and its manifest.
46    Generate {
47        /// Registry name of the target.
48        target: String,
49        /// Directory that receives the generated files.
50        output: PathBuf,
51        /// Permit replacement of the target's deterministic filenames.
52        force: bool,
53    },
54    /// Generate and elaborate the supported smoke-check targets with Icarus,
55    /// optionally adding Verilator lint and host-oracle arithmetic simulations
56    /// for the unchecked complete SHA lane and exact three-lane farm.
57    Check {
58        /// Parent for the unique temporary check directory.
59        temp_root: Option<PathBuf>,
60        /// Preserve the unique directory after a successful or failed check.
61        keep: bool,
62        /// Also render the complete SHA lane and exact three-lane farm, then run
63        /// Verilator lint/elaboration and deterministic arithmetic simulations.
64        full: bool,
65        /// Sequentially render and link the modular production signer, then run
66        /// its fail-closed structural and generated-RTL equivalence gates.
67        signer_full: bool,
68    },
69    /// Build and run the separately generated sustained-throughput testbench
70    /// against an existing immutable successful production-signer bundle.
71    SignerThroughput {
72        /// Existing directory containing the complete signer check-evidence
73        /// manifest and every immutable file in its checksum chain.
74        source_evidence: PathBuf,
75        /// New directory that receives the immutable throughput evidence.
76        output: PathBuf,
77    },
78    /// Generate the immutable modular RHDL benchmark, metadata-derived Vitis
79    /// wrapper, and packaging Tcl in sequential lowering children.
80    BenchmarkKernel {
81        /// New directory that receives the frozen generation bundle.
82        output: PathBuf,
83    },
84    /// Bind the exact generated HBM kernel and successful signer evidence into
85    /// one immutable graph record.
86    U280LinkGraph {
87        /// Generated Rust/RHDL shell plus metadata-derived adapter source.
88        kernel: PathBuf,
89        /// Canonical schema-2 generation manifest adjacent to the kernel.
90        kernel_manifest: PathBuf,
91        /// Successful immutable full-signer evidence directory.
92        signer_evidence: PathBuf,
93        /// New read-only checksum/graph JSON record.
94        output: PathBuf,
95    },
96    /// Revalidate an existing immutable U280 link record without writing files.
97    U280VerifyLinkGraph {
98        /// Generated Rust/RHDL shell plus metadata-derived adapter source.
99        kernel: PathBuf,
100        /// Canonical schema-2 generation manifest adjacent to the kernel.
101        kernel_manifest: PathBuf,
102        /// Successful immutable full-signer evidence directory.
103        signer_evidence: PathBuf,
104        /// Existing canonical read-only schema-2 link record.
105        record: PathBuf,
106    },
107    /// Run a source-tier external generated-RTL gate over the actual HBM top,
108    /// actual signer bundle, AXI4-Lite, and deterministic HBM models.
109    U280HbmRtlCheck {
110        /// Generated Rust/RHDL shell plus metadata-derived adapter source.
111        kernel: PathBuf,
112        /// Canonical schema-2 generation manifest adjacent to the kernel.
113        kernel_manifest: PathBuf,
114        /// Successful immutable full-signer evidence directory.
115        signer_evidence: PathBuf,
116        /// Existing canonical read-only schema-2 live link record.
117        record: PathBuf,
118        /// New directory that receives the immutable external-RTL evidence.
119        output: PathBuf,
120    },
121    /// Internal one-graph signer lowering operation used only by the parent
122    /// `check --signer-full` process.
123    #[doc(hidden)]
124    InternalSignerLower {
125        /// Exact graph to lower: `lane`, `skeleton`, or `benchmark-skeleton`.
126        kind: String,
127        /// New immutable source path written by the child.
128        output: PathBuf,
129        /// Clean parent commit that the child must independently rediscover.
130        source_commit: String,
131    },
132    /// Print usage text.
133    Help,
134}
135
136/// Metadata and generator for one extensible RTL target.
137#[derive(Clone, Copy)]
138pub struct TargetSpec {
139    /// Stable command-line name.
140    pub name: &'static str,
141    /// Deterministic generated Verilog filename.
142    pub verilog_filename: &'static str,
143    /// Deterministic provenance-manifest filename.
144    pub manifest_filename: &'static str,
145    /// Cryptographic profile identifier.
146    pub profile: &'static str,
147    /// Top module expected in the generated Verilog.
148    pub top_module: &'static str,
149    /// Short human-facing description.
150    pub description: &'static str,
151    /// Validation boundary attached to this target and copied into manifests.
152    pub validation_scope: &'static str,
153    /// Whether ordinary unit-test generation loops may lower this target.
154    pub default_unit_generation: bool,
155    generate: fn() -> Result<String>,
156}
157
158impl std::fmt::Debug for TargetSpec {
159    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        formatter
161            .debug_struct("TargetSpec")
162            .field("name", &self.name)
163            .field("verilog_filename", &self.verilog_filename)
164            .field("manifest_filename", &self.manifest_filename)
165            .field("profile", &self.profile)
166            .field("top_module", &self.top_module)
167            .field("description", &self.description)
168            .field("validation_scope", &self.validation_scope)
169            .field("default_unit_generation", &self.default_unit_generation)
170            .finish_non_exhaustive()
171    }
172}
173
174fn reject_standalone_signer_generation() -> Result<String> {
175    bail!(
176        "the modular production signer has multiple immutable sources; use `rhdl-gen check --signer-full --keep`"
177    )
178}
179
180static SIGNER_FULL_TARGET: TargetSpec = TargetSpec {
181    name: full_signer_check::SIGNER_TARGET_NAME,
182    verilog_filename: "wots_sha256_signer_top.bundle.v",
183    manifest_filename: "wots_sha256_signer_top.bundle.manifest.json",
184    profile: "HASHSIGS_SHA256_GENERIC_V1",
185    top_module: full_signer_check::SIGNER_TOP_MODULE,
186    description: "modular complete SHA-256 three-cluster signer bundle",
187    validation_scope: "opt-in structural validation and finite generated-RTL equivalence only",
188    default_unit_generation: false,
189    generate: reject_standalone_signer_generation,
190};
191
192fn generate_keccak_round() -> Result<String> {
193    keccak_rhdl::generate_round_verilog().context("lowering the isolated Keccak round")
194}
195
196fn generate_keccak_permutation() -> Result<String> {
197    keccak_rhdl::generate_permutation_verilog()
198        .context("lowering the complete Keccak permutation oracle")
199}
200
201fn generate_keccak_iterative() -> Result<String> {
202    keccak_rhdl::generate_iterative_verilog().context("lowering the iterative Keccak engine")
203}
204
205fn combinational_kernel_module(
206    module_name: &str,
207    ports: &str,
208    declarations: &str,
209    function: &str,
210    assignment: &str,
211) -> String {
212    format!(
213        "module {module_name}({ports});\n{declarations}\n\n{function}\n\n{assignment}\nendmodule\n"
214    )
215}
216
217fn generate_sha256_round() -> Result<String> {
218    let function = sha256_rhdl::round_verilog().context("lowering the isolated SHA-256 round")?;
219    Ok(combinational_kernel_module(
220        "sha256_round_ooc",
221        "state_i, round_constant_i, state_o",
222        "  input wire [767:0] state_i;\n  input wire [31:0] round_constant_i;\n  output wire [767:0] state_o;",
223        &function,
224        "  assign state_o = kernel_round_kernel(state_i, round_constant_i);",
225    ))
226}
227
228fn generate_sha256_compression() -> Result<String> {
229    let function = sha256_rhdl::compression_verilog()
230        .context("lowering the complete combinational SHA-256 compression oracle")?;
231    Ok(combinational_kernel_module(
232        "sha256_compression_ooc",
233        "chaining_i, block_i, digest_o",
234        "  input wire [255:0] chaining_i;\n  input wire [511:0] block_i;\n  output wire [255:0] digest_o;",
235        &function,
236        "  assign digest_o = kernel_compression_kernel(chaining_i, block_i);",
237    ))
238}
239
240fn generate_sha256_pipeline_2() -> Result<String> {
241    sha256_rhdl::round_pipeline_verilog::<2>()
242        .context("lowering the checked two-stage SHA-256 round pipeline")
243}
244
245fn generate_sha256_pipeline_64_unchecked() -> Result<String> {
246    sha256_rhdl::round_pipeline_unchecked_verilog::<64>()
247        .context("rendering the unchecked 64-stage SHA-256 round pipeline")
248}
249
250fn generate_sha256_compression_lane_64_unchecked() -> Result<String> {
251    sha256_rhdl::lane::try_full_compression_lane_unchecked_verilog()
252        .context("rendering the unchecked complete 64-round SHA-256 compression lane")
253}
254
255fn generate_sha256_compression_farm_3x64_unchecked() -> Result<String> {
256    sha256_rhdl::farm::try_full_three_lane_farm_generation_only_unchecked_verilog()
257        .context("rendering the unchecked exact three-lane SHA-256 compression farm")
258}
259
260/// Registered RHDL Verilog generation targets.
261///
262/// New profile-specific targets should be added here without introducing a
263/// runtime profile mux into any cryptographic datapath.
264pub static TARGETS: &[TargetSpec] = &[
265    TargetSpec {
266        name: "legacy-keccak-round",
267        verilog_filename: "legacy_keccak_round.v",
268        manifest_filename: "legacy_keccak_round.manifest.json",
269        profile: "LEGACY_KECCAK",
270        top_module: "keccak_round_ooc",
271        description: "compile-time round-0 cell for isolated RTL evaluation",
272        validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
273        default_unit_generation: true,
274        generate: generate_keccak_round,
275    },
276    TargetSpec {
277        name: "legacy-keccak-permutation",
278        verilog_filename: "legacy_keccak_permutation.v",
279        manifest_filename: "legacy_keccak_permutation.manifest.json",
280        profile: "LEGACY_KECCAK",
281        top_module: "keccak_f1600_ooc",
282        description: "24-round combinational correctness oracle",
283        validation_scope: "export-only in the default check; no synthesis or hardware claim",
284        default_unit_generation: true,
285        generate: generate_keccak_permutation,
286    },
287    TargetSpec {
288        name: "legacy-keccak-iterative",
289        verilog_filename: "legacy_keccak_iterative.v",
290        manifest_filename: "legacy_keccak_iterative.manifest.json",
291        profile: "LEGACY_KECCAK",
292        top_module: "keccak_iterative",
293        description: "one-round-per-cycle 24-round engine",
294        validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
295        default_unit_generation: true,
296        generate: generate_keccak_iterative,
297    },
298    TargetSpec {
299        name: "sha256-round",
300        verilog_filename: "sha256_round.v",
301        manifest_filename: "sha256_round.manifest.json",
302        profile: "HASHSIGS_SHA256_GENERIC_V1",
303        top_module: "sha256_round_ooc",
304        description: "isolated combinational SHA-256 round with an explicit round constant",
305        validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
306        default_unit_generation: true,
307        generate: generate_sha256_round,
308    },
309    TargetSpec {
310        name: "sha256-compression-oracle",
311        verilog_filename: "sha256_compression_oracle.v",
312        manifest_filename: "sha256_compression_oracle.manifest.json",
313        profile: "HASHSIGS_SHA256_GENERIC_V1",
314        top_module: "sha256_compression_ooc",
315        description: "slow full combinational SHA-256 compression oracle",
316        validation_scope: "slow export-only target; excluded from default generation and Icarus checks",
317        default_unit_generation: false,
318        generate: generate_sha256_compression,
319    },
320    TargetSpec {
321        name: "sha256-pipeline-2",
322        verilog_filename: "sha256_pipeline_2.v",
323        manifest_filename: "sha256_pipeline_2.manifest.json",
324        profile: "HASHSIGS_SHA256_GENERIC_V1",
325        top_module: "sha256_round_pipeline",
326        description: "checked two-stage registered SHA-256 round-pipeline prefix",
327        validation_scope: "RHDL-checked generation plus Icarus syntax/elaboration in the default check",
328        default_unit_generation: true,
329        generate: generate_sha256_pipeline_2,
330    },
331    TargetSpec {
332        name: "sha256-pipeline-64-unchecked",
333        verilog_filename: "sha256_pipeline_64_unchecked.v",
334        manifest_filename: "sha256_pipeline_64_unchecked.manifest.json",
335        profile: "HASHSIGS_SHA256_GENERIC_V1",
336        top_module: "sha256_round_pipeline",
337        description: "unchecked rendering of the complete 64-stage SHA-256 round pipeline; not equivalence evidence",
338        validation_scope: "unchecked RHDL rendering; Verilator lint/control-only simulation are not RHDL/RTL equivalence evidence",
339        default_unit_generation: false,
340        generate: generate_sha256_pipeline_64_unchecked,
341    },
342    TargetSpec {
343        name: "sha256-compression-lane-64-unchecked",
344        verilog_filename: "sha256_compression_lane_64_unchecked.v",
345        manifest_filename: "sha256_compression_lane_64_unchecked.manifest.json",
346        profile: "HASHSIGS_SHA256_GENERIC_V1",
347        top_module: "sha256_compression_lane",
348        description: "complete 64-round BRAM-backed SHA-256 compression lane",
349        validation_scope: "unchecked RHDL rendering; `check --full` adds Verilator lint and arithmetic simulation, but no synthesis, timing, route, or hardware claim",
350        default_unit_generation: false,
351        generate: generate_sha256_compression_lane_64_unchecked,
352    },
353    TargetSpec {
354        name: "sha256-compression-farm-3x64-unchecked",
355        verilog_filename: "sha256_compression_farm_3x64_unchecked.v",
356        manifest_filename: "sha256_compression_farm_3x64_unchecked.manifest.json",
357        profile: "HASHSIGS_SHA256_GENERIC_V1",
358        top_module: "sha256_compression_farm_3x64",
359        description: "exact ThreeLaneCompressionFarm with independent complete SHA-256 lanes",
360        validation_scope: "unchecked RHDL rendering; `check --full` adds Verilator lint and cross-lane arithmetic simulation, but no synthesis, timing, route, or hardware claim",
361        default_unit_generation: false,
362        generate: generate_sha256_compression_farm_3x64_unchecked,
363    },
364    TargetSpec {
365        name: u280_hbm::U280_HBM_TARGET_NAME,
366        verilog_filename: "hashsigs_sha256_hbm_kernel.v",
367        manifest_filename: "hashsigs_sha256_hbm_kernel.manifest.json",
368        profile: "HASHSIGS_SHA256_GENERIC_V1",
369        top_module: u280_hbm::U280_HBM_TOP_MODULE,
370        description: "Rust/RHDL U280 HBM transport shell with an unresolved modular signer",
371        validation_scope: "metadata coverage and wire-only adapter validation; generation alone is not parse, simulation, synthesis, timing, route, resource, or hardware evidence",
372        default_unit_generation: false,
373        generate: u280_hbm::generate,
374    },
375];
376
377/// Source-tree identity captured before an artifact is written.
378#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
379pub struct SourceProvenance {
380    /// Git commit containing the source baseline.
381    pub commit: String,
382    /// Whether tracked or untracked source-tree changes were present.
383    pub dirty: bool,
384}
385
386impl SourceProvenance {
387    /// Inspect the source worktree containing this crate without taking a Git
388    /// optional lock.
389    ///
390    /// # Errors
391    ///
392    /// Returns an error if Git is unavailable or the source is not in a Git
393    /// worktree.
394    pub fn discover() -> Result<Self> {
395        let source_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
396        let commit = git_output(source_dir, ["rev-parse", "HEAD"])?;
397        let status = git_output(
398            source_dir,
399            ["status", "--porcelain=v1", "--untracked-files=normal"],
400        )?;
401        Ok(Self {
402            commit,
403            dirty: !status.is_empty(),
404        })
405    }
406}
407
408/// Deterministic checksum and provenance record accompanying generated RTL.
409#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct ArtifactManifest {
412    /// Manifest schema revision.
413    pub schema_version: u32,
414    /// Stable target registry name.
415    pub target: String,
416    /// Cryptographic profile identifier.
417    pub profile: String,
418    /// Evidence tier; always `generated-verilog` in this tool.
419    pub evidence_tier: String,
420    /// Exact validation boundary declared by the selected target.
421    pub validation_scope: String,
422    /// Generated Verilog filename relative to the manifest.
423    pub verilog_file: String,
424    /// SHA-256 digest of the exact generated Verilog bytes.
425    pub verilog_sha256: String,
426    /// Source Git commit captured before output creation.
427    pub source_git_commit: String,
428    /// Source dirty state captured before output creation.
429    pub source_git_dirty: bool,
430    /// Pinned RHDL revision.
431    pub rhdl_revision: String,
432    /// Pinned `HashSigsRS` revision.
433    pub hashsigs_revision: String,
434    /// Generator package version.
435    pub generator_version: String,
436}
437
438/// Files produced by one generation operation.
439#[derive(Clone, Debug, Eq, PartialEq)]
440pub struct GeneratedArtifact {
441    /// Generated Verilog path.
442    pub verilog_path: PathBuf,
443    /// Generated JSON manifest path.
444    pub manifest_path: PathBuf,
445    /// Parsed manifest written alongside the Verilog.
446    pub manifest: ArtifactManifest,
447}
448
449/// Parse arguments excluding the executable name.
450///
451/// # Errors
452///
453/// Returns a descriptive error for missing, duplicate, or unknown arguments.
454pub fn parse_args<I, S>(args: I) -> Result<Command>
455where
456    I: IntoIterator<Item = S>,
457    S: Into<OsString>,
458{
459    let mut args = args.into_iter().map(Into::into);
460    let Some(operation) = args.next() else {
461        bail!("missing command");
462    };
463    match operation.to_str() {
464        Some("list") => {
465            ensure_no_extra(args, "list")?;
466            Ok(Command::List)
467        }
468        Some("generate") => parse_generate(args),
469        Some("check") => parse_check(args),
470        Some("signer-throughput") => parse_signer_throughput(args),
471        Some("benchmark-kernel") => parse_benchmark_kernel(args),
472        Some("u280-link-graph") => parse_u280_link_graph(args),
473        Some("u280-verify-link-graph") => parse_u280_verify_link_graph(args),
474        Some("u280-hbm-rtl-check") => parse_u280_hbm_rtl_check(args),
475        Some("__signer-lower") => parse_internal_signer_lower(args),
476        Some("help" | "--help" | "-h") => {
477            ensure_no_extra(args, "help")?;
478            Ok(Command::Help)
479        }
480        Some(other) => bail!("unknown command `{other}`"),
481        None => bail!("command is not valid UTF-8"),
482    }
483}
484
485fn parse_benchmark_kernel(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
486    if args.next().as_deref() != Some(std::ffi::OsStr::new("--output")) {
487        bail!("benchmark-kernel requires --output <NEW_DIRECTORY>");
488    }
489    let output = PathBuf::from(
490        args.next()
491            .ok_or_else(|| anyhow!("benchmark-kernel requires --output <NEW_DIRECTORY>"))?,
492    );
493    ensure_no_extra(args, "benchmark-kernel")?;
494    Ok(Command::BenchmarkKernel { output })
495}
496
497fn parse_u280_link_graph(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
498    let mut kernel = None;
499    let mut kernel_manifest = None;
500    let mut signer_evidence = None;
501    let mut output = None;
502    while let Some(argument) = args.next() {
503        let (slot, description) = match argument.to_str() {
504            Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
505            Some("--kernel-manifest") => (
506                &mut kernel_manifest,
507                "--kernel-manifest requires a JSON file",
508            ),
509            Some("--signer-evidence") => (
510                &mut signer_evidence,
511                "--signer-evidence requires a directory",
512            ),
513            Some("--output") => (&mut output, "--output requires a new JSON file"),
514            Some(other) => bail!("unknown u280-link-graph argument `{other}`"),
515            None => bail!("u280-link-graph option is not valid UTF-8"),
516        };
517        if slot.is_some() {
518            bail!("{} may only be specified once", argument.to_string_lossy());
519        }
520        *slot = Some(PathBuf::from(
521            args.next().ok_or_else(|| anyhow!(description))?,
522        ));
523    }
524    Ok(Command::U280LinkGraph {
525        kernel: kernel.ok_or_else(|| anyhow!("u280-link-graph requires --kernel <VERILOG>"))?,
526        kernel_manifest: kernel_manifest
527            .ok_or_else(|| anyhow!("u280-link-graph requires --kernel-manifest <JSON>"))?,
528        signer_evidence: signer_evidence
529            .ok_or_else(|| anyhow!("u280-link-graph requires --signer-evidence <DIRECTORY>"))?,
530        output: output.ok_or_else(|| anyhow!("u280-link-graph requires --output <NEW_JSON>"))?,
531    })
532}
533
534fn parse_u280_verify_link_graph(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
535    let mut kernel = None;
536    let mut kernel_manifest = None;
537    let mut signer_evidence = None;
538    let mut record = None;
539    while let Some(argument) = args.next() {
540        let (slot, description) = match argument.to_str() {
541            Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
542            Some("--kernel-manifest") => (
543                &mut kernel_manifest,
544                "--kernel-manifest requires a JSON file",
545            ),
546            Some("--signer-evidence") => (
547                &mut signer_evidence,
548                "--signer-evidence requires a directory",
549            ),
550            Some("--record") => (&mut record, "--record requires an existing JSON file"),
551            Some(other) => bail!("unknown u280-verify-link-graph argument `{other}`"),
552            None => bail!("u280-verify-link-graph option is not valid UTF-8"),
553        };
554        if slot.is_some() {
555            bail!("{} may only be specified once", argument.to_string_lossy());
556        }
557        *slot = Some(PathBuf::from(
558            args.next().ok_or_else(|| anyhow!(description))?,
559        ));
560    }
561    Ok(Command::U280VerifyLinkGraph {
562        kernel: kernel
563            .ok_or_else(|| anyhow!("u280-verify-link-graph requires --kernel <VERILOG>"))?,
564        kernel_manifest: kernel_manifest
565            .ok_or_else(|| anyhow!("u280-verify-link-graph requires --kernel-manifest <JSON>"))?,
566        signer_evidence: signer_evidence.ok_or_else(|| {
567            anyhow!("u280-verify-link-graph requires --signer-evidence <DIRECTORY>")
568        })?,
569        record: record.ok_or_else(|| anyhow!("u280-verify-link-graph requires --record <JSON>"))?,
570    })
571}
572
573fn parse_u280_hbm_rtl_check(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
574    let mut kernel = None;
575    let mut kernel_manifest = None;
576    let mut signer_evidence = None;
577    let mut record = None;
578    let mut output = None;
579    while let Some(argument) = args.next() {
580        let (slot, description) = match argument.to_str() {
581            Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
582            Some("--kernel-manifest") => (
583                &mut kernel_manifest,
584                "--kernel-manifest requires a JSON file",
585            ),
586            Some("--signer-evidence") => (
587                &mut signer_evidence,
588                "--signer-evidence requires a directory",
589            ),
590            Some("--record") => (&mut record, "--record requires an existing JSON file"),
591            Some("--output") => (&mut output, "--output requires a new directory"),
592            Some(other) => bail!("unknown u280-hbm-rtl-check argument `{other}`"),
593            None => bail!("u280-hbm-rtl-check option is not valid UTF-8"),
594        };
595        if slot.is_some() {
596            bail!("{} may only be specified once", argument.to_string_lossy());
597        }
598        *slot = Some(PathBuf::from(
599            args.next().ok_or_else(|| anyhow!(description))?,
600        ));
601    }
602    Ok(Command::U280HbmRtlCheck {
603        kernel: kernel.ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --kernel <VERILOG>"))?,
604        kernel_manifest: kernel_manifest
605            .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --kernel-manifest <JSON>"))?,
606        signer_evidence: signer_evidence
607            .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --signer-evidence <DIRECTORY>"))?,
608        record: record.ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --record <JSON>"))?,
609        output: output
610            .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --output <NEW_DIRECTORY>"))?,
611    })
612}
613
614fn parse_signer_throughput(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
615    let mut source_evidence = None;
616    let mut output = None;
617    while let Some(argument) = args.next() {
618        match argument.to_str() {
619            Some("--source-evidence") => {
620                if source_evidence.is_some() {
621                    bail!("--source-evidence may only be specified once");
622                }
623                source_evidence =
624                    Some(PathBuf::from(args.next().ok_or_else(|| {
625                        anyhow!("--source-evidence requires a directory")
626                    })?));
627            }
628            Some("--output" | "-o") => {
629                if output.is_some() {
630                    bail!("--output may only be specified once");
631                }
632                output =
633                    Some(PathBuf::from(args.next().ok_or_else(|| {
634                        anyhow!("--output requires a new directory")
635                    })?));
636            }
637            Some(other) => bail!("unknown signer-throughput argument `{other}`"),
638            None => bail!("signer-throughput option is not valid UTF-8"),
639        }
640    }
641    Ok(Command::SignerThroughput {
642        source_evidence: source_evidence
643            .ok_or_else(|| anyhow!("signer-throughput requires --source-evidence <DIRECTORY>"))?,
644        output: output
645            .ok_or_else(|| anyhow!("signer-throughput requires --output <NEW_DIRECTORY>"))?,
646    })
647}
648
649fn parse_internal_signer_lower(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
650    let kind = required_utf8(args.next(), "internal signer lowering kind")?;
651    if !matches!(kind.as_str(), "lane" | "skeleton" | "benchmark-skeleton") {
652        bail!("unknown internal signer lowering kind `{kind}`");
653    }
654    if args.next().as_deref() != Some(std::ffi::OsStr::new("--output")) {
655        bail!("internal signer lowerer requires --output <FILE>");
656    }
657    let output = PathBuf::from(
658        args.next()
659            .ok_or_else(|| anyhow!("internal signer lowerer requires an output file"))?,
660    );
661    if args.next().as_deref() != Some(std::ffi::OsStr::new("--source-commit")) {
662        bail!("internal signer lowerer requires --source-commit <COMMIT>");
663    }
664    let source_commit = required_utf8(args.next(), "internal signer source commit")?;
665    ensure_no_extra(args, "internal signer lowerer")?;
666    Ok(Command::InternalSignerLower {
667        kind,
668        output,
669        source_commit,
670    })
671}
672
673fn parse_generate(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
674    let target = required_utf8(args.next(), "generation target")?;
675    let mut output = None;
676    let mut force = false;
677    while let Some(argument) = args.next() {
678        match argument.to_str() {
679            Some("--output" | "-o") => {
680                if output.is_some() {
681                    bail!("--output may only be specified once");
682                }
683                output = Some(PathBuf::from(
684                    args.next()
685                        .ok_or_else(|| anyhow!("--output requires a directory"))?,
686                ));
687            }
688            Some("--force") if !force => force = true,
689            Some("--force") => bail!("--force may only be specified once"),
690            Some(other) => bail!("unknown generate argument `{other}`"),
691            None => bail!("generate option is not valid UTF-8"),
692        }
693    }
694    Ok(Command::Generate {
695        target,
696        output: output.ok_or_else(|| anyhow!("generate requires --output <DIRECTORY>"))?,
697        force,
698    })
699}
700
701fn parse_check(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
702    let mut temp_root = None;
703    let mut keep = false;
704    let mut full = false;
705    let mut signer_full = false;
706    while let Some(argument) = args.next() {
707        match argument.to_str() {
708            Some("--temp-root") => {
709                if temp_root.is_some() {
710                    bail!("--temp-root may only be specified once");
711                }
712                temp_root =
713                    Some(PathBuf::from(args.next().ok_or_else(|| {
714                        anyhow!("--temp-root requires a directory")
715                    })?));
716            }
717            Some("--keep") if !keep => keep = true,
718            Some("--keep") => bail!("--keep may only be specified once"),
719            Some("--full") if !full => full = true,
720            Some("--full") => bail!("--full may only be specified once"),
721            Some("--signer-full") if !signer_full => signer_full = true,
722            Some("--signer-full") => bail!("--signer-full may only be specified once"),
723            Some(other) => bail!("unknown check argument `{other}`"),
724            None => bail!("check option is not valid UTF-8"),
725        }
726    }
727    Ok(Command::Check {
728        temp_root,
729        keep,
730        full,
731        signer_full,
732    })
733}
734
735fn required_utf8(value: Option<OsString>, description: &str) -> Result<String> {
736    let value = value.ok_or_else(|| anyhow!("missing {description}"))?;
737    value
738        .into_string()
739        .map_err(|_| anyhow!("{description} is not valid UTF-8"))
740}
741
742fn ensure_no_extra(mut args: impl Iterator<Item = OsString>, operation: &str) -> Result<()> {
743    if let Some(extra) = args.next() {
744        bail!(
745            "{operation} does not accept argument `{}`",
746            extra.to_string_lossy()
747        );
748    }
749    Ok(())
750}
751
752/// Parse and execute arguments excluding the executable name.
753///
754/// # Errors
755///
756/// Returns argument, generation, filesystem, Git, Icarus, or Verilator
757/// diagnostics.
758pub fn run_from_args<I, S>(args: I) -> Result<String>
759where
760    I: IntoIterator<Item = S>,
761    S: Into<OsString>,
762{
763    execute(parse_args(args)?)
764}
765
766/// Execute one parsed command and return its human-readable output.
767///
768/// # Errors
769///
770/// Returns generation, filesystem, Git, Icarus, or Verilator diagnostics.
771pub fn execute(command: Command) -> Result<String> {
772    match command {
773        Command::List => Ok(render_target_list()),
774        Command::Help => Ok(USAGE.to_owned()),
775        Command::Generate {
776            target,
777            output,
778            force,
779        } => {
780            let spec = find_target(&target)?;
781            let provenance = SourceProvenance::discover()?;
782            let artifact = generate_target(spec, &output, force, &provenance)?;
783            Ok(format!(
784                "generated {}\nmanifest {}\nevidence tier: {}",
785                artifact.verilog_path.display(),
786                artifact.manifest_path.display(),
787                GENERATED_VERILOG_EVIDENCE_TIER
788            ))
789        }
790        Command::Check {
791            temp_root,
792            keep,
793            full,
794            signer_full,
795        } => run_check(temp_root.as_deref(), keep, full, signer_full),
796        Command::SignerThroughput {
797            source_evidence,
798            output,
799        } => run_signer_throughput_check(&source_evidence, &output),
800        Command::BenchmarkKernel { output } => {
801            benchmark_kernel::run_benchmark_kernel_generation(&output)
802        }
803        Command::U280LinkGraph {
804            kernel,
805            kernel_manifest,
806            signer_evidence,
807            output,
808        } => {
809            let record = u280_hbm_check::write_immutable_combined_link_record(
810                &kernel,
811                &kernel_manifest,
812                &signer_evidence,
813                &output,
814            )?;
815            Ok(format!(
816                "validated immutable provenance-bound U280 link graph\nrecord {}\nkernel sha256 {}\nsigner sha256 {}\nreachable modules {}\nevidence boundary: exact current generated source, successful finite signer evidence, and structural reachability only; not synthesis, timing, route, resource, or hardware evidence",
817                output.display(),
818                record.kernel.sha256,
819                record.signer_bundle.sha256,
820                record.modules.len()
821            ))
822        }
823        Command::U280VerifyLinkGraph {
824            kernel,
825            kernel_manifest,
826            signer_evidence,
827            record,
828        } => {
829            let validated = u280_hbm_check::verify_immutable_combined_link_record(
830                &kernel,
831                &kernel_manifest,
832                &signer_evidence,
833                &record,
834            )?;
835            Ok(format!(
836                "verified immutable provenance-bound U280 link graph\nrecord {}\nkernel sha256 {}\nsigner sha256 {}\nreachable modules {}\nno files written",
837                record.display(),
838                validated.kernel.sha256,
839                validated.signer_bundle.sha256,
840                validated.modules.len()
841            ))
842        }
843        Command::U280HbmRtlCheck {
844            kernel,
845            kernel_manifest,
846            signer_evidence,
847            record,
848            output,
849        } => u280_hbm_rtl_check::run(
850            &kernel,
851            &kernel_manifest,
852            &signer_evidence,
853            &record,
854            &output,
855        ),
856        Command::InternalSignerLower {
857            kind,
858            output,
859            source_commit,
860        } => full_signer_check::run_internal_lowerer(&kind, &output, &source_commit),
861    }
862}
863
864/// Look up a target by its stable registry name.
865///
866/// # Errors
867///
868/// Returns an error listing valid target names when no match exists.
869pub fn find_target(name: &str) -> Result<&'static TargetSpec> {
870    TARGETS
871        .iter()
872        .find(|target| target.name == name)
873        .ok_or_else(|| {
874            anyhow!(
875                "unknown target `{name}`; available targets: {}",
876                TARGETS
877                    .iter()
878                    .map(|target| target.name)
879                    .collect::<Vec<_>>()
880                    .join(", ")
881            )
882        })
883}
884
885fn render_target_list() -> String {
886    let mut output = String::from("TARGET\tPROFILE\tTOP\tVALIDATION\tDESCRIPTION\n");
887    for target in TARGETS {
888        let _ = writeln!(
889            output,
890            "{}\t{}\t{}\t{}\t{}",
891            target.name,
892            target.profile,
893            target.top_module,
894            target.validation_scope,
895            target.description
896        );
897    }
898    output.pop();
899    output
900}
901
902/// Generate one target and its deterministic provenance manifest.
903///
904/// Source provenance must be captured before this function is called.  If
905/// either deterministic destination exists, generation fails unless `force` is
906/// true.
907///
908/// # Errors
909///
910/// Returns an error on lowering, serialization, directory, overwrite, or file
911/// write failures.
912pub fn generate_target(
913    target: &TargetSpec,
914    output_dir: &Path,
915    force: bool,
916    provenance: &SourceProvenance,
917) -> Result<GeneratedArtifact> {
918    fs::create_dir_all(output_dir).with_context(|| {
919        format!(
920            "creating selected output directory {}",
921            output_dir.display()
922        )
923    })?;
924    let verilog_path = output_dir.join(target.verilog_filename);
925    let manifest_path = output_dir.join(target.manifest_filename);
926    check_overwrite(&verilog_path, force)?;
927    check_overwrite(&manifest_path, force)?;
928
929    let mut verilog = (target.generate)()?;
930    if !verilog.ends_with('\n') {
931        verilog.push('\n');
932    }
933    let digest = hex::encode(Sha256::digest(verilog.as_bytes()));
934    let manifest = ArtifactManifest {
935        schema_version: 2,
936        target: target.name.to_owned(),
937        profile: target.profile.to_owned(),
938        evidence_tier: GENERATED_VERILOG_EVIDENCE_TIER.to_owned(),
939        validation_scope: target.validation_scope.to_owned(),
940        verilog_file: target.verilog_filename.to_owned(),
941        verilog_sha256: digest,
942        source_git_commit: provenance.commit.clone(),
943        source_git_dirty: provenance.dirty,
944        rhdl_revision: RHDL_REVISION.to_owned(),
945        hashsigs_revision: HASHSIGS_REVISION.to_owned(),
946        generator_version: env!("CARGO_PKG_VERSION").to_owned(),
947    };
948    let mut manifest_json =
949        serde_json::to_string_pretty(&manifest).context("serializing the provenance manifest")?;
950    manifest_json.push('\n');
951
952    write_output(&verilog_path, verilog.as_bytes(), force)?;
953    if let Err(error) = write_output(&manifest_path, manifest_json.as_bytes(), force) {
954        if !force {
955            let _ = fs::remove_file(&verilog_path);
956        }
957        return Err(error);
958    }
959    Ok(GeneratedArtifact {
960        verilog_path,
961        manifest_path,
962        manifest,
963    })
964}
965
966fn check_overwrite(path: &Path, force: bool) -> Result<()> {
967    if path.exists() && !force {
968        bail!(
969            "refusing to overwrite {}; pass --force to replace this target's files",
970            path.display()
971        );
972    }
973    Ok(())
974}
975
976fn write_output(path: &Path, content: &[u8], force: bool) -> Result<()> {
977    let mut options = OpenOptions::new();
978    options.write(true);
979    if force {
980        options.create(true).truncate(true);
981    } else {
982        options.create_new(true);
983    }
984    let mut file = options
985        .open(path)
986        .with_context(|| format!("opening output {}", path.display()))?;
987    file.write_all(content)
988        .with_context(|| format!("writing output {}", path.display()))?;
989    file.sync_all()
990        .with_context(|| format!("syncing output {}", path.display()))
991}
992
993fn git_output<const N: usize>(source_dir: &Path, arguments: [&str; N]) -> Result<String> {
994    let output = ProcessCommand::new("git")
995        .env("GIT_OPTIONAL_LOCKS", "0")
996        .arg("--no-optional-locks")
997        .arg("-C")
998        .arg(source_dir)
999        .args(arguments)
1000        .output()
1001        .context("running Git for source provenance")?;
1002    if !output.status.success() {
1003        bail!(
1004            "Git source-provenance command failed: {}",
1005            String::from_utf8_lossy(&output.stderr).trim()
1006        );
1007    }
1008    String::from_utf8(output.stdout)
1009        .context("Git source-provenance output is not UTF-8")
1010        .map(|output| output.trim().to_owned())
1011}
1012
1013fn run_check(
1014    temp_root: Option<&Path>,
1015    keep: bool,
1016    full: bool,
1017    signer_full: bool,
1018) -> Result<String> {
1019    let provenance = SourceProvenance::discover()?;
1020    if signer_full && provenance.dirty {
1021        bail!(
1022            "`rhdl-gen check --signer-full` requires a clean source worktree; commit or remove source changes before producing immutable RTL evidence"
1023        );
1024    }
1025    let root = temp_root.map_or_else(std::env::temp_dir, Path::to_path_buf);
1026    fs::create_dir_all(&root)
1027        .with_context(|| format!("creating selected temporary root {}", root.display()))?;
1028    let workspace = CheckWorkspace::create(&root, keep)?;
1029    require_iverilog(workspace.path())?;
1030    let quick_targets = [
1031        "legacy-keccak-round",
1032        "legacy-keccak-iterative",
1033        "sha256-round",
1034        "sha256-pipeline-2",
1035    ];
1036    for target_name in quick_targets {
1037        let target = find_target(target_name)?;
1038        let artifact = generate_target(target, workspace.path(), false, &provenance)?;
1039        elaborate_with_iverilog(target, &artifact.verilog_path, workspace.path())?;
1040    }
1041    let full_report = if full {
1042        let full_targets = [
1043            (
1044                "sha256-compression-lane-64-unchecked",
1045                "sha256_lane_arithmetic_tb",
1046                full_sha_check::lane_testbench(),
1047                full_sha_check::LANE_PASS_MARKER,
1048            ),
1049            (
1050                "sha256-compression-farm-3x64-unchecked",
1051                "sha256_farm_arithmetic_tb",
1052                full_sha_check::farm_testbench(),
1053                full_sha_check::FARM_PASS_MARKER,
1054            ),
1055        ];
1056        let mut reports = Vec::with_capacity(full_targets.len() * 2);
1057        for (target_name, testbench_top, testbench, pass_marker) in full_targets {
1058            let target = find_target(target_name)?;
1059            let artifact = generate_target(target, workspace.path(), false, &provenance)?;
1060            reports.push(lint_with_verilator(
1061                target,
1062                &artifact.verilog_path,
1063                workspace.path(),
1064                keep,
1065            )?);
1066            reports.push(simulate_sha_arithmetic_with_verilator(
1067                target,
1068                &artifact.verilog_path,
1069                workspace.path(),
1070                keep,
1071                testbench_top,
1072                &testbench,
1073                pass_marker,
1074            )?);
1075        }
1076        Some(reports.join("\n"))
1077    } else {
1078        None
1079    };
1080    let signer_report = if signer_full {
1081        Some(run_signer_full_check(workspace.path(), keep, &provenance)?)
1082    } else {
1083        None
1084    };
1085    let disposition = if keep {
1086        format!("kept at {}", workspace.path().display())
1087    } else {
1088        "removed after success".to_owned()
1089    };
1090    let evidence_tier = GENERATED_VERILOG_EVIDENCE_TIER;
1091    let mut report = format!(
1092        "Icarus syntax/elaboration checks passed for {}\nevidence boundary: syntax/elaboration only; not simulation or equivalence evidence",
1093        quick_targets.join(", ")
1094    );
1095    if let Some(full_report) = full_report {
1096        let _ = write!(report, "\n{full_report}");
1097    }
1098    if let Some(signer_report) = signer_report {
1099        let _ = write!(report, "\n{signer_report}");
1100    }
1101    let _ = write!(
1102        report,
1103        "\ntemporary workspace: {disposition}\nevidence tier: {evidence_tier}"
1104    );
1105    Ok(report)
1106}
1107
1108fn run_signer_full_check(
1109    output_dir: &Path,
1110    keep: bool,
1111    provenance: &SourceProvenance,
1112) -> Result<String> {
1113    let artifact = full_signer_check::generate_bundle(output_dir, provenance)?;
1114    full_signer_check::validate_persisted_bundle(&artifact)?;
1115    let lint = lint_with_verilator(&SIGNER_FULL_TARGET, &artifact.bundle_path, output_dir, keep)?;
1116    let lint_log_path = output_dir.join(format!(
1117        "verilator-lint-{}.log",
1118        full_signer_check::SIGNER_TARGET_NAME
1119    ));
1120    let object_dir = output_dir.join(format!(
1121        "verilator_sim_{}",
1122        full_signer_check::SIGNER_TARGET_NAME
1123    ));
1124
1125    full_signer_check::validate_persisted_bundle(&artifact)?;
1126    let build = build_verilator_testbench(VerilatorBuildRequest {
1127        target: &SIGNER_FULL_TARGET,
1128        verilog: &artifact.bundle_path,
1129        output_dir,
1130        keep,
1131        testbench_top: full_signer_check::SIGNER_TESTBENCH_TOP,
1132        testbench_path: &artifact.testbench_path,
1133        object_dir: &object_dir,
1134        evidence_label: "signer",
1135    })?;
1136    full_signer_check::validate_persisted_bundle(&artifact)?;
1137    let simulation = run_verilator_testbench(
1138        &SIGNER_FULL_TARGET,
1139        output_dir,
1140        keep,
1141        full_signer_check::SIGNER_TESTBENCH_TOP,
1142        full_signer_check::SIGNER_PASS_MARKER,
1143        &object_dir,
1144        "signer",
1145    )?;
1146    full_signer_check::validate_persisted_bundle(&artifact)?;
1147    let evidence_manifest = full_signer_check::write_check_evidence_manifest(
1148        &artifact,
1149        &lint_log_path,
1150        &build.log_path,
1151        &simulation.log_path,
1152    )?;
1153
1154    Ok(format!(
1155        "{lint}\nVerilator modular production-signer simulation passed with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: exact finite-vector generated-RTL equivalence for one reset-discarded job and twelve completed jobs, including saturation, frame bytes, job identity, ready/valid stability, final hold, releases, and quiescence; not formal proof, sustained-throughput, synthesis, timing, route, resource, or hardware evidence\nimmutable lane source: {}\nimmutable signer skeleton: {}\nimmutable linked bundle: {}\nimmutable bundle manifest: {}\nimmutable signer testbench: {}\nimmutable lowering logs: {}, {}\nimmutable check-evidence manifest: {}\nVerilator signer logs: {}, {}",
1156        full_signer_check::SIGNER_PASS_MARKER,
1157        build.elapsed.as_secs_f64(),
1158        simulation.elapsed.as_secs_f64(),
1159        render_artifact_location(&artifact.lane_path, keep),
1160        render_artifact_location(&artifact.skeleton_path, keep),
1161        render_artifact_location(&artifact.bundle_path, keep),
1162        render_artifact_location(&artifact.manifest_path, keep),
1163        render_artifact_location(&artifact.testbench_path, keep),
1164        render_artifact_location(&artifact.lane_lowering_log_path, keep),
1165        render_artifact_location(&artifact.skeleton_lowering_log_path, keep),
1166        render_artifact_location(&evidence_manifest, keep),
1167        render_artifact_location(&build.log_path, keep),
1168        render_artifact_location(&simulation.log_path, keep),
1169    ))
1170}
1171
1172fn create_throughput_output_directory(selected: &Path, source_evidence: &Path) -> Result<PathBuf> {
1173    ensure!(
1174        !selected.exists(),
1175        "signer-throughput output already exists; select a new directory: {}",
1176        selected.display()
1177    );
1178    let file_name = selected
1179        .file_name()
1180        .filter(|name| !name.is_empty())
1181        .ok_or_else(|| anyhow!("signer-throughput output must name a new child directory"))?;
1182    let parent = selected
1183        .parent()
1184        .filter(|parent| !parent.as_os_str().is_empty())
1185        .unwrap_or_else(|| Path::new("."));
1186    ensure!(
1187        parent.is_dir(),
1188        "signer-throughput output parent does not exist: {}",
1189        parent.display()
1190    );
1191    let parent = fs::canonicalize(parent)
1192        .with_context(|| format!("canonicalizing output parent {}", parent.display()))?;
1193    let output = parent.join(file_name);
1194    ensure!(
1195        !output.starts_with(source_evidence),
1196        "signer-throughput output must not modify the immutable source-evidence directory"
1197    );
1198    let repository = Path::new(env!("CARGO_MANIFEST_DIR"))
1199        .parent()
1200        .and_then(Path::parent)
1201        .context("rhdl-gen crate is not nested beneath the workspace root")?;
1202    let repository = fs::canonicalize(repository)
1203        .context("canonicalizing the signer-throughput flow workspace")?;
1204    ensure!(
1205        !output.starts_with(&repository),
1206        "signer-throughput output must be outside the source workspace"
1207    );
1208    fs::create_dir(&output)
1209        .with_context(|| format!("creating signer-throughput output {}", output.display()))?;
1210    fs::canonicalize(&output).with_context(|| {
1211        format!(
1212            "canonicalizing signer-throughput output {}",
1213            output.display()
1214        )
1215    })
1216}
1217
1218fn require_verilator_version(output_dir: &Path) -> Result<String> {
1219    let output = ProcessCommand::new("verilator")
1220        .env("TMPDIR", output_dir)
1221        .env("TMP", output_dir)
1222        .env("TEMP", output_dir)
1223        .arg("--version")
1224        .output()
1225        .map_err(|error| {
1226            if error.kind() == std::io::ErrorKind::NotFound {
1227                anyhow!(
1228                    "Verilator is required for `rhdl-gen signer-throughput` but was not found in PATH"
1229                )
1230            } else {
1231                anyhow!(error)
1232            }
1233        })?;
1234    ensure!(
1235        output.status.success(),
1236        "Verilator is present but `verilator --version` failed: {}",
1237        String::from_utf8_lossy(&output.stderr).trim()
1238    );
1239    let version = String::from_utf8(output.stdout).context("Verilator version is not UTF-8")?;
1240    ensure!(
1241        !version.trim().is_empty(),
1242        "Verilator returned an empty version"
1243    );
1244    Ok(version.trim().to_owned())
1245}
1246
1247fn run_signer_throughput_check(source_evidence: &Path, output: &Path) -> Result<String> {
1248    let flow_provenance = SourceProvenance::discover()?;
1249    ensure!(
1250        !flow_provenance.dirty,
1251        "`rhdl-gen signer-throughput` requires a clean flow worktree so its evidence binds committed code"
1252    );
1253    let source = full_signer_check::validate_successful_evidence_directory(source_evidence)?;
1254    let output_dir = create_throughput_output_directory(output, &source.directory)?;
1255    let flow_executable = std::env::current_exe().context("locating the rhdl-gen executable")?;
1256    let flow_executable_record = full_signer_check::file_record(&flow_executable)?;
1257    let verilator_version = require_verilator_version(&output_dir)?;
1258    let testbench_path = signer_throughput_check::write_throughput_testbench(&output_dir)?;
1259    let testbench_record = full_signer_check::file_record(&testbench_path)?;
1260    let object_dir = output_dir.join("verilator_sim_sha256-signer-sustained-throughput");
1261
1262    ensure!(
1263        full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1264        "signer source evidence changed before throughput build"
1265    );
1266    let build = build_verilator_testbench(VerilatorBuildRequest {
1267        target: &SIGNER_FULL_TARGET,
1268        verilog: &source.bundle_path,
1269        output_dir: &output_dir,
1270        keep: true,
1271        testbench_top: signer_throughput_check::THROUGHPUT_TESTBENCH_TOP,
1272        testbench_path: &testbench_path,
1273        object_dir: &object_dir,
1274        evidence_label: "signer-throughput",
1275    })?;
1276    ensure!(
1277        full_signer_check::file_record(&testbench_path)? == testbench_record,
1278        "throughput testbench changed during Verilator build"
1279    );
1280    ensure!(
1281        full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1282        "signer source evidence changed during throughput build"
1283    );
1284    let simulator = object_dir.join(format!(
1285        "V{}",
1286        signer_throughput_check::THROUGHPUT_TESTBENCH_TOP
1287    ));
1288    let simulator_record = full_signer_check::file_record(&simulator)?;
1289    let simulation = run_verilator_testbench(
1290        &SIGNER_FULL_TARGET,
1291        &output_dir,
1292        true,
1293        signer_throughput_check::THROUGHPUT_TESTBENCH_TOP,
1294        signer_throughput_check::THROUGHPUT_PASS_MARKER,
1295        &object_dir,
1296        "signer-throughput",
1297    )?;
1298    ensure!(
1299        full_signer_check::file_record(&simulator)? == simulator_record,
1300        "Verilator throughput simulator changed while it was running"
1301    );
1302    ensure!(
1303        full_signer_check::file_record(&testbench_path)? == testbench_record,
1304        "throughput testbench changed during simulation"
1305    );
1306    ensure!(
1307        full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1308        "signer source evidence changed during throughput simulation"
1309    );
1310    let final_flow_provenance = SourceProvenance::discover()?;
1311    ensure!(
1312        final_flow_provenance == flow_provenance && !final_flow_provenance.dirty,
1313        "throughput flow source changed during the external build or simulation"
1314    );
1315    ensure!(
1316        full_signer_check::file_record(&flow_executable)? == flow_executable_record,
1317        "rhdl-gen executable changed during the throughput gate"
1318    );
1319    let manifest = signer_throughput_check::write_throughput_manifest(
1320        &output_dir,
1321        &signer_throughput_check::ThroughputManifestInputs {
1322            source: &source,
1323            flow_provenance: &flow_provenance,
1324            flow_executable: &flow_executable,
1325            verilator_version: &verilator_version,
1326            testbench: &testbench_path,
1327            simulator: &simulator,
1328            build_log: &build.log_path,
1329            simulation_log: &simulation.log_path,
1330        },
1331    )?;
1332
1333    Ok(format!(
1334        "Verilator sustained signer throughput simulation passed with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: 72 exact fused private-seed oracle frames under continuous admission and always-ready output; completion boundaries 12 through 60 must span at most 24,000 cycles (500 cycles/result normalized at 250 MHz); not formal proof, synthesis, timing, route, resource, or hardware evidence\nimmutable source signer bundle: {}\nimmutable source bundle/check-evidence manifests: {}, {}\nimmutable throughput testbench: {}\nread-only throughput simulator: {}\nimmutable throughput build/simulation logs: {}, {}\nimmutable throughput evidence manifest: {}",
1335        signer_throughput_check::THROUGHPUT_PASS_MARKER,
1336        build.elapsed.as_secs_f64(),
1337        simulation.elapsed.as_secs_f64(),
1338        source.bundle_path.display(),
1339        source.bundle_manifest_path.display(),
1340        source.check_evidence_manifest_path.display(),
1341        testbench_path.display(),
1342        simulator.display(),
1343        build.log_path.display(),
1344        simulation.log_path.display(),
1345        manifest.display(),
1346    ))
1347}
1348
1349fn lint_with_verilator(
1350    target: &TargetSpec,
1351    verilog: &Path,
1352    output_dir: &Path,
1353    keep: bool,
1354) -> Result<String> {
1355    let version = ProcessCommand::new("verilator")
1356        .env("TMPDIR", output_dir)
1357        .env("TMP", output_dir)
1358        .env("TEMP", output_dir)
1359        .arg("--version")
1360        .output()
1361        .map_err(|error| {
1362            if error.kind() == std::io::ErrorKind::NotFound {
1363                anyhow!(
1364                    "Verilator is required for `rhdl-gen check --full` or `--signer-full` but was not found in PATH"
1365                )
1366            } else {
1367                anyhow!(error)
1368            }
1369        })?;
1370    if !version.status.success() {
1371        bail!(
1372            "Verilator is present but `verilator --version` failed: {}",
1373            String::from_utf8_lossy(&version.stderr).trim()
1374        );
1375    }
1376    let version = String::from_utf8_lossy(&version.stdout).trim().to_owned();
1377    let object_dir = output_dir.join(format!("verilator_lint_{}", target.name));
1378    let started = Instant::now();
1379    let output = ProcessCommand::new("verilator")
1380        .env("TMPDIR", output_dir)
1381        .env("TMP", output_dir)
1382        .env("TEMP", output_dir)
1383        .arg("--lint-only")
1384        .arg("-Wall")
1385        .arg("-Wno-fatal")
1386        .arg("--threads")
1387        .arg("1")
1388        .arg("--top-module")
1389        .arg(target.top_module)
1390        .arg("--Mdir")
1391        .arg(&object_dir)
1392        .arg(verilog)
1393        .output()
1394        .with_context(|| format!("invoking Verilator for target {}", target.name))?;
1395    let elapsed = started.elapsed();
1396    let stdout = String::from_utf8_lossy(&output.stdout);
1397    let stderr = String::from_utf8_lossy(&output.stderr);
1398    let warning_inventory = verilator_warning_inventory(&stderr);
1399    let warning_count = warning_inventory.values().sum::<usize>();
1400    let warning_summary = render_warning_inventory(&warning_inventory);
1401    let log_path = output_dir.join(format!("verilator-lint-{}.log", target.name));
1402    let log = format!(
1403        "command: verilator --lint-only -Wall -Wno-fatal --threads 1 --top-module {} --Mdir {} {}\nversion: {version}\nelapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1404        target.top_module,
1405        object_dir.display(),
1406        verilog.display(),
1407        elapsed.as_secs_f64(),
1408        output.status,
1409        stdout.trim(),
1410        stderr.trim(),
1411    );
1412    fs::write(&log_path, log)
1413        .with_context(|| format!("writing Verilator diagnostic log {}", log_path.display()))?;
1414    if !output.status.success() {
1415        bail!(
1416            "Verilator lint/elaboration failed for target {} (top {}) after {:.3}s; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1417            target.name,
1418            target.top_module,
1419            elapsed.as_secs_f64(),
1420            render_artifact_location(&log_path, keep),
1421            stdout.trim(),
1422            stderr.trim()
1423        );
1424    }
1425    Ok(format!(
1426        "Verilator lint/elaboration passed for {} (top {}) with {version} in {:.3}s; accepted warning inventory: {} total ({}) (captured in log)\nevidence boundary: unchecked rendering plus lint/elaboration only; not simulation or equivalence evidence; warning acceptance is not a warning-clean claim\nVerilator log: {}",
1427        target.name,
1428        target.top_module,
1429        elapsed.as_secs_f64(),
1430        warning_count,
1431        warning_summary,
1432        render_artifact_location(&log_path, keep)
1433    ))
1434}
1435
1436fn verilator_warning_inventory(stderr: &str) -> BTreeMap<String, usize> {
1437    let mut inventory = BTreeMap::new();
1438    for code in stderr.lines().filter_map(|line| {
1439        line.strip_prefix("%Warning-")
1440            .and_then(|warning| warning.split_once(':'))
1441            .map(|(code, _)| code)
1442    }) {
1443        *inventory.entry(code.to_owned()).or_default() += 1;
1444    }
1445    inventory
1446}
1447
1448fn render_warning_inventory(inventory: &BTreeMap<String, usize>) -> String {
1449    if inventory.is_empty() {
1450        return "none".to_owned();
1451    }
1452    let mut summary = String::new();
1453    for (index, (code, count)) in inventory.iter().enumerate() {
1454        if index != 0 {
1455            summary.push_str(", ");
1456        }
1457        write!(summary, "{code}={count}").expect("writing to a String cannot fail");
1458    }
1459    summary
1460}
1461
1462fn simulate_sha_arithmetic_with_verilator(
1463    target: &TargetSpec,
1464    verilog: &Path,
1465    output_dir: &Path,
1466    keep: bool,
1467    testbench_top: &str,
1468    testbench: &str,
1469    pass_marker: &str,
1470) -> Result<String> {
1471    let testbench_path = output_dir.join(format!("{testbench_top}.sv"));
1472    fs::write(&testbench_path, testbench).with_context(|| {
1473        format!(
1474            "writing SHA arithmetic testbench {}",
1475            testbench_path.display()
1476        )
1477    })?;
1478    let object_dir = output_dir.join(format!("verilator_sim_{}", target.name));
1479    let build = build_verilator_testbench(VerilatorBuildRequest {
1480        target,
1481        verilog,
1482        output_dir,
1483        keep,
1484        testbench_top,
1485        testbench_path: &testbench_path,
1486        object_dir: &object_dir,
1487        evidence_label: "arithmetic",
1488    })?;
1489    let simulation = run_verilator_testbench(
1490        target,
1491        output_dir,
1492        keep,
1493        testbench_top,
1494        pass_marker,
1495        &object_dir,
1496        "arithmetic",
1497    )?;
1498    Ok(format!(
1499        "Verilator generated-RTL arithmetic simulation passed for {} with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: deterministic host-model equivalence, exact latency, tag, bubble, and reset-valid behavior for the exercised vectors only; not formal proof, synthesis, timing, route, or hardware evidence\nVerilator testbench: {}\nVerilator simulation logs: {}, {}",
1500        target.name,
1501        pass_marker,
1502        build.elapsed.as_secs_f64(),
1503        simulation.elapsed.as_secs_f64(),
1504        render_artifact_location(&testbench_path, keep),
1505        render_artifact_location(&build.log_path, keep),
1506        render_artifact_location(&simulation.log_path, keep)
1507    ))
1508}
1509
1510struct VerilatorPhaseEvidence {
1511    elapsed: Duration,
1512    log_path: PathBuf,
1513}
1514
1515#[derive(Clone, Copy)]
1516struct VerilatorBuildRequest<'a> {
1517    target: &'a TargetSpec,
1518    verilog: &'a Path,
1519    output_dir: &'a Path,
1520    keep: bool,
1521    testbench_top: &'a str,
1522    testbench_path: &'a Path,
1523    object_dir: &'a Path,
1524    evidence_label: &'a str,
1525}
1526
1527fn build_verilator_testbench(request: VerilatorBuildRequest<'_>) -> Result<VerilatorPhaseEvidence> {
1528    let VerilatorBuildRequest {
1529        target,
1530        verilog,
1531        output_dir,
1532        keep,
1533        testbench_top,
1534        testbench_path,
1535        object_dir,
1536        evidence_label,
1537    } = request;
1538    let build_started = Instant::now();
1539    let build = ProcessCommand::new("verilator")
1540        .env("TMPDIR", output_dir)
1541        .env("TMP", output_dir)
1542        .env("TEMP", output_dir)
1543        .arg("--binary")
1544        .arg("--timing")
1545        .arg("-Wno-fatal")
1546        .arg("-Wno-DECLFILENAME")
1547        .arg("-Wno-UNUSEDSIGNAL")
1548        .arg("--threads")
1549        .arg("1")
1550        .arg("-j")
1551        .arg("1")
1552        .arg("--top-module")
1553        .arg(testbench_top)
1554        .arg("--Mdir")
1555        .arg(object_dir)
1556        .arg(verilog)
1557        .arg(testbench_path)
1558        .output()
1559        .with_context(|| {
1560            format!(
1561                "building the Verilator {evidence_label} testbench for {}",
1562                target.name,
1563            )
1564        })?;
1565    let build_elapsed = build_started.elapsed();
1566    let build_stdout = String::from_utf8_lossy(&build.stdout);
1567    let build_stderr = String::from_utf8_lossy(&build.stderr);
1568    let build_log_path = output_dir.join(format!(
1569        "verilator-{evidence_label}-build-{}.log",
1570        target.name
1571    ));
1572    let build_log = format!(
1573        "command: verilator --binary --timing -Wno-fatal -Wno-DECLFILENAME -Wno-UNUSEDSIGNAL --threads 1 -j 1 --top-module {testbench_top} --Mdir {} {} {}\nelapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1574        object_dir.display(),
1575        verilog.display(),
1576        testbench_path.display(),
1577        build_elapsed.as_secs_f64(),
1578        build.status,
1579        build_stdout.trim(),
1580        build_stderr.trim()
1581    );
1582    fs::write(&build_log_path, build_log).with_context(|| {
1583        format!(
1584            "writing {evidence_label} simulation build log {}",
1585            build_log_path.display()
1586        )
1587    })?;
1588    if !build.status.success() {
1589        bail!(
1590            "Verilator could not build the {evidence_label} testbench for {} after {:.3}s; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1591            target.name,
1592            build_elapsed.as_secs_f64(),
1593            render_artifact_location(&build_log_path, keep),
1594            build_stdout.trim(),
1595            build_stderr.trim()
1596        );
1597    }
1598    Ok(VerilatorPhaseEvidence {
1599        elapsed: build_elapsed,
1600        log_path: build_log_path,
1601    })
1602}
1603
1604fn run_verilator_testbench(
1605    target: &TargetSpec,
1606    output_dir: &Path,
1607    keep: bool,
1608    testbench_top: &str,
1609    pass_marker: &str,
1610    object_dir: &Path,
1611    evidence_label: &str,
1612) -> Result<VerilatorPhaseEvidence> {
1613    let executable = object_dir.join(format!("V{testbench_top}"));
1614    let simulation_started = Instant::now();
1615    let simulation = ProcessCommand::new(&executable)
1616        .env("TMPDIR", output_dir)
1617        .env("TMP", output_dir)
1618        .env("TEMP", output_dir)
1619        .output()
1620        .with_context(|| {
1621            format!(
1622                "running Verilator {evidence_label} simulation {}",
1623                executable.display(),
1624            )
1625        })?;
1626    let simulation_elapsed = simulation_started.elapsed();
1627    let simulation_stdout = String::from_utf8_lossy(&simulation.stdout);
1628    let simulation_stderr = String::from_utf8_lossy(&simulation.stderr);
1629    let simulation_log_path = output_dir.join(format!(
1630        "verilator-{evidence_label}-simulation-{}.log",
1631        target.name,
1632    ));
1633    let simulation_log = format!(
1634        "elapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1635        simulation_elapsed.as_secs_f64(),
1636        simulation.status,
1637        simulation_stdout.trim(),
1638        simulation_stderr.trim()
1639    );
1640    fs::write(&simulation_log_path, simulation_log).with_context(|| {
1641        format!(
1642            "writing {evidence_label} simulation log {}",
1643            simulation_log_path.display()
1644        )
1645    })?;
1646    if !simulation.status.success() || !simulation_stdout.contains(pass_marker) {
1647        bail!(
1648            "Verilator {evidence_label} simulation failed for {} after {:.3}s; expected marker `{}`; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1649            target.name,
1650            simulation_elapsed.as_secs_f64(),
1651            pass_marker,
1652            render_artifact_location(&simulation_log_path, keep),
1653            simulation_stdout.trim(),
1654            simulation_stderr.trim()
1655        );
1656    }
1657    Ok(VerilatorPhaseEvidence {
1658        elapsed: simulation_elapsed,
1659        log_path: simulation_log_path,
1660    })
1661}
1662
1663fn render_artifact_location(path: &Path, keep: bool) -> String {
1664    if keep {
1665        format!("{} (preserved by --keep)", path.display())
1666    } else {
1667        format!(
1668            "{} (ephemeral; removed when the check exits, use --keep to preserve)",
1669            path.display()
1670        )
1671    }
1672}
1673
1674fn require_iverilog(temp_dir: &Path) -> Result<()> {
1675    let output = ProcessCommand::new("iverilog")
1676        .env("TMPDIR", temp_dir)
1677        .env("TMP", temp_dir)
1678        .env("TEMP", temp_dir)
1679        .arg("-V")
1680        .output()
1681        .map_err(|error| {
1682            if error.kind() == std::io::ErrorKind::NotFound {
1683                anyhow!(
1684                    "Icarus Verilog (`iverilog`) is required for `rhdl-gen check` but was not found in PATH"
1685                )
1686            } else {
1687                anyhow!(error)
1688            }
1689        })?;
1690    if !output.status.success() {
1691        bail!(
1692            "Icarus Verilog is present but `iverilog -V` failed: {}",
1693            String::from_utf8_lossy(&output.stderr).trim()
1694        );
1695    }
1696    Ok(())
1697}
1698
1699fn elaborate_with_iverilog(target: &TargetSpec, verilog: &Path, output_dir: &Path) -> Result<()> {
1700    let elaborated = output_dir.join(format!("{}.ivl", target.name));
1701    let output = ProcessCommand::new("iverilog")
1702        .env("TMPDIR", output_dir)
1703        .env("TMP", output_dir)
1704        .env("TEMP", output_dir)
1705        .arg("-g2012")
1706        .arg("-s")
1707        .arg(target.top_module)
1708        .arg("-o")
1709        .arg(&elaborated)
1710        .arg(verilog)
1711        .output()
1712        .with_context(|| format!("invoking Icarus for target {}", target.name))?;
1713    if !output.status.success() {
1714        bail!(
1715            "Icarus syntax/elaboration failed for target {} (top {}):\nstdout:\n{}\nstderr:\n{}",
1716            target.name,
1717            target.top_module,
1718            String::from_utf8_lossy(&output.stdout).trim(),
1719            String::from_utf8_lossy(&output.stderr).trim()
1720        );
1721    }
1722    Ok(())
1723}
1724
1725static CHECK_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
1726
1727struct CheckWorkspace {
1728    path: PathBuf,
1729    keep: bool,
1730}
1731
1732impl CheckWorkspace {
1733    fn create(root: &Path, keep: bool) -> Result<Self> {
1734        for _ in 0..100 {
1735            let counter = CHECK_DIRECTORY_COUNTER.fetch_add(1, Ordering::Relaxed);
1736            let path = root.join(format!("rhdl-gen-check-{}-{counter}", std::process::id()));
1737            match fs::create_dir(&path) {
1738                Ok(()) => {
1739                    let canonical = fs::canonicalize(&path).with_context(|| {
1740                        format!(
1741                            "canonicalizing temporary check directory {}",
1742                            path.display()
1743                        )
1744                    });
1745                    let path = match canonical {
1746                        Ok(path) => path,
1747                        Err(error) => {
1748                            let _ = fs::remove_dir(&path);
1749                            return Err(error);
1750                        }
1751                    };
1752                    return Ok(Self { path, keep });
1753                }
1754                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
1755                Err(error) => {
1756                    return Err(error).with_context(|| {
1757                        format!("creating temporary check directory {}", path.display())
1758                    });
1759                }
1760            }
1761        }
1762        bail!(
1763            "could not allocate a unique check directory beneath {}",
1764            root.display()
1765        )
1766    }
1767
1768    fn path(&self) -> &Path {
1769        &self.path
1770    }
1771}
1772
1773impl Drop for CheckWorkspace {
1774    fn drop(&mut self) {
1775        if !self.keep {
1776            let _ = fs::remove_dir_all(&self.path);
1777        }
1778    }
1779}
1780
1781#[cfg(test)]
1782mod tests {
1783    use super::*;
1784
1785    static TEST_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
1786
1787    struct TestDirectory(PathBuf);
1788
1789    impl TestDirectory {
1790        fn new(label: &str) -> Self {
1791            let counter = TEST_DIRECTORY_COUNTER.fetch_add(1, Ordering::Relaxed);
1792            let path = std::env::temp_dir().join(format!(
1793                "rhdl-gen-test-{}-{counter}-{label}",
1794                std::process::id()
1795            ));
1796            fs::create_dir(&path).expect("create selected test directory");
1797            Self(path)
1798        }
1799    }
1800
1801    impl Drop for TestDirectory {
1802        fn drop(&mut self) {
1803            let _ = fs::remove_dir_all(&self.0);
1804        }
1805    }
1806
1807    fn fixture_provenance() -> SourceProvenance {
1808        SourceProvenance {
1809            commit: "0123456789abcdef0123456789abcdef01234567".to_owned(),
1810            dirty: false,
1811        }
1812    }
1813
1814    #[allow(
1815        clippy::too_many_lines,
1816        reason = "the supported-command matrix is kept in one test so every parsed variant remains auditable together"
1817    )]
1818    #[test]
1819    fn parses_supported_commands() {
1820        assert_eq!(parse_args(["list"]).expect("list parses"), Command::List);
1821        assert_eq!(
1822            parse_args([
1823                "generate",
1824                "legacy-keccak-round",
1825                "--output",
1826                "/tmp/example",
1827                "--force",
1828            ])
1829            .expect("generate parses"),
1830            Command::Generate {
1831                target: "legacy-keccak-round".to_owned(),
1832                output: PathBuf::from("/tmp/example"),
1833                force: true,
1834            }
1835        );
1836        assert_eq!(
1837            parse_args(["check", "--full", "--temp-root", "/tmp", "--keep"]).expect("check parses"),
1838            Command::Check {
1839                temp_root: Some(PathBuf::from("/tmp")),
1840                keep: true,
1841                full: true,
1842                signer_full: false,
1843            }
1844        );
1845        assert_eq!(
1846            parse_args(["check", "--signer-full"]).expect("signer check parses"),
1847            Command::Check {
1848                temp_root: None,
1849                keep: false,
1850                full: false,
1851                signer_full: true,
1852            }
1853        );
1854        assert_eq!(
1855            parse_args([
1856                "signer-throughput",
1857                "--source-evidence",
1858                "/tmp/source",
1859                "--output",
1860                "/tmp/output",
1861            ])
1862            .expect("signer throughput parses"),
1863            Command::SignerThroughput {
1864                source_evidence: PathBuf::from("/tmp/source"),
1865                output: PathBuf::from("/tmp/output"),
1866            }
1867        );
1868        assert_eq!(
1869            parse_args(["benchmark-kernel", "--output", "/tmp/benchmark"])
1870                .expect("benchmark kernel parses"),
1871            Command::BenchmarkKernel {
1872                output: PathBuf::from("/tmp/benchmark"),
1873            }
1874        );
1875        assert_eq!(
1876            parse_args([
1877                "u280-link-graph",
1878                "--kernel",
1879                "/tmp/kernel.v",
1880                "--kernel-manifest",
1881                "/tmp/kernel.manifest.json",
1882                "--signer-evidence",
1883                "/tmp/signer-evidence",
1884                "--output",
1885                "/tmp/combined.json",
1886            ])
1887            .expect("U280 link graph parses"),
1888            Command::U280LinkGraph {
1889                kernel: PathBuf::from("/tmp/kernel.v"),
1890                kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1891                signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1892                output: PathBuf::from("/tmp/combined.json"),
1893            }
1894        );
1895        assert_eq!(
1896            parse_args([
1897                "u280-verify-link-graph",
1898                "--kernel",
1899                "/tmp/kernel.v",
1900                "--kernel-manifest",
1901                "/tmp/kernel.manifest.json",
1902                "--signer-evidence",
1903                "/tmp/signer-evidence",
1904                "--record",
1905                "/tmp/combined.json",
1906            ])
1907            .expect("U280 link graph verifier parses"),
1908            Command::U280VerifyLinkGraph {
1909                kernel: PathBuf::from("/tmp/kernel.v"),
1910                kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1911                signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1912                record: PathBuf::from("/tmp/combined.json"),
1913            }
1914        );
1915        assert_eq!(
1916            parse_args([
1917                "u280-hbm-rtl-check",
1918                "--kernel",
1919                "/tmp/kernel.v",
1920                "--kernel-manifest",
1921                "/tmp/kernel.manifest.json",
1922                "--signer-evidence",
1923                "/tmp/signer-evidence",
1924                "--record",
1925                "/tmp/combined.json",
1926                "--output",
1927                "/tmp/rtl-evidence",
1928            ])
1929            .expect("U280 HBM RTL check parses"),
1930            Command::U280HbmRtlCheck {
1931                kernel: PathBuf::from("/tmp/kernel.v"),
1932                kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1933                signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1934                record: PathBuf::from("/tmp/combined.json"),
1935                output: PathBuf::from("/tmp/rtl-evidence"),
1936            }
1937        );
1938        assert_eq!(
1939            parse_args([
1940                "__signer-lower",
1941                "lane",
1942                "--output",
1943                "/tmp/lane.v",
1944                "--source-commit",
1945                "0123456789abcdef",
1946            ])
1947            .expect("internal signer lowerer parses"),
1948            Command::InternalSignerLower {
1949                kind: "lane".to_owned(),
1950                output: PathBuf::from("/tmp/lane.v"),
1951                source_commit: "0123456789abcdef".to_owned(),
1952            }
1953        );
1954    }
1955
1956    #[allow(
1957        clippy::too_many_lines,
1958        reason = "the rejected-command matrix is kept in one test so incomplete and duplicate cases remain auditable together"
1959    )]
1960    #[test]
1961    fn rejects_incomplete_or_duplicate_arguments() {
1962        assert!(parse_args(["generate", "legacy-keccak-round"]).is_err());
1963        assert!(
1964            parse_args([
1965                "generate",
1966                "legacy-keccak-round",
1967                "-o",
1968                "one",
1969                "--output",
1970                "two",
1971            ])
1972            .is_err()
1973        );
1974        assert!(parse_args(["list", "extra"]).is_err());
1975        assert!(parse_args(["unknown"]).is_err());
1976        assert!(parse_args(["check", "--full", "--full"]).is_err());
1977        assert!(parse_args(["check", "--signer-full", "--signer-full"]).is_err());
1978        assert!(parse_args(["signer-throughput", "--output", "one"]).is_err());
1979        assert!(parse_args(["benchmark-kernel"]).is_err());
1980        assert!(parse_args(["benchmark-kernel", "--output", "one", "extra"]).is_err());
1981        assert!(
1982            parse_args([
1983                "u280-link-graph",
1984                "--kernel",
1985                "kernel.v",
1986                "--output",
1987                "combined.json",
1988            ])
1989            .is_err()
1990        );
1991        assert!(
1992            parse_args([
1993                "u280-link-graph",
1994                "--kernel",
1995                "one.v",
1996                "--kernel",
1997                "two.v",
1998                "--kernel-manifest",
1999                "kernel.json",
2000                "--signer-evidence",
2001                "signer-evidence",
2002                "--output",
2003                "combined.json",
2004            ])
2005            .is_err()
2006        );
2007        assert!(
2008            parse_args([
2009                "u280-verify-link-graph",
2010                "--kernel",
2011                "kernel.v",
2012                "--kernel-manifest",
2013                "kernel.json",
2014                "--signer-evidence",
2015                "signer-evidence",
2016            ])
2017            .is_err()
2018        );
2019        assert!(
2020            parse_args([
2021                "u280-hbm-rtl-check",
2022                "--kernel",
2023                "kernel.v",
2024                "--kernel-manifest",
2025                "kernel.json",
2026                "--signer-evidence",
2027                "signer-evidence",
2028                "--record",
2029                "combined.json",
2030            ])
2031            .is_err()
2032        );
2033        assert!(
2034            parse_args([
2035                "u280-hbm-rtl-check",
2036                "--kernel",
2037                "one.v",
2038                "--kernel",
2039                "two.v",
2040                "--kernel-manifest",
2041                "kernel.json",
2042                "--signer-evidence",
2043                "signer-evidence",
2044                "--record",
2045                "combined.json",
2046                "--output",
2047                "evidence",
2048            ])
2049            .is_err()
2050        );
2051        assert!(
2052            parse_args([
2053                "u280-verify-link-graph",
2054                "--kernel",
2055                "kernel.v",
2056                "--kernel-manifest",
2057                "one.json",
2058                "--kernel-manifest",
2059                "two.json",
2060                "--signer-evidence",
2061                "signer-evidence",
2062                "--record",
2063                "combined.json",
2064            ])
2065            .is_err()
2066        );
2067        assert!(
2068            parse_args([
2069                "signer-throughput",
2070                "--source-evidence",
2071                "one",
2072                "--source-evidence",
2073                "two",
2074                "--output",
2075                "three",
2076            ])
2077            .is_err()
2078        );
2079        assert!(parse_args(["__signer-lower", "farm"]).is_err());
2080    }
2081
2082    #[test]
2083    fn registry_is_profile_explicit_and_easy_to_extend() {
2084        let expected_names = [
2085            "legacy-keccak-round",
2086            "legacy-keccak-permutation",
2087            "legacy-keccak-iterative",
2088            "sha256-round",
2089            "sha256-compression-oracle",
2090            "sha256-pipeline-2",
2091            "sha256-pipeline-64-unchecked",
2092            "sha256-compression-lane-64-unchecked",
2093            "sha256-compression-farm-3x64-unchecked",
2094            "sha256-u280-hbm-kernel",
2095        ];
2096        assert_eq!(TARGETS.len(), 10);
2097        assert_eq!(
2098            TARGETS.iter().map(|target| target.name).collect::<Vec<_>>(),
2099            expected_names.to_vec()
2100        );
2101        for target in TARGETS {
2102            assert!(matches!(
2103                target.profile,
2104                "LEGACY_KECCAK" | "HASHSIGS_SHA256_GENERIC_V1"
2105            ));
2106            assert!(
2107                Path::new(target.verilog_filename)
2108                    .extension()
2109                    .is_some_and(|extension| extension.eq_ignore_ascii_case("v"))
2110            );
2111            assert!(target.manifest_filename.ends_with(".manifest.json"));
2112            assert!(!target.validation_scope.is_empty());
2113        }
2114        assert_eq!(
2115            find_target("sha256-pipeline-64-unchecked")
2116                .expect("unchecked target is registered")
2117                .profile,
2118            "HASHSIGS_SHA256_GENERIC_V1"
2119        );
2120    }
2121
2122    #[test]
2123    fn generation_is_deterministic_and_manifest_digest_matches() {
2124        let first = TestDirectory::new("deterministic-first");
2125        let second = TestDirectory::new("deterministic-second");
2126        let target = find_target("legacy-keccak-round").expect("registered target");
2127        let provenance = fixture_provenance();
2128        let first_artifact =
2129            generate_target(target, &first.0, false, &provenance).expect("first generation");
2130        let second_artifact =
2131            generate_target(target, &second.0, false, &provenance).expect("second generation");
2132
2133        let first_verilog = fs::read(&first_artifact.verilog_path).expect("read first Verilog");
2134        let second_verilog = fs::read(&second_artifact.verilog_path).expect("read second Verilog");
2135        assert_eq!(first_verilog, second_verilog);
2136        assert_eq!(
2137            fs::read(&first_artifact.manifest_path).expect("read first manifest"),
2138            fs::read(&second_artifact.manifest_path).expect("read second manifest")
2139        );
2140        assert_eq!(
2141            first_artifact.manifest.verilog_sha256,
2142            hex::encode(Sha256::digest(&first_verilog))
2143        );
2144        assert_eq!(
2145            first_artifact.manifest.evidence_tier,
2146            GENERATED_VERILOG_EVIDENCE_TIER
2147        );
2148        assert_eq!(
2149            first_artifact.manifest.validation_scope,
2150            target.validation_scope
2151        );
2152        assert_eq!(first_artifact.manifest.source_git_commit, provenance.commit);
2153        assert!(!first_artifact.manifest.source_git_dirty);
2154    }
2155
2156    #[test]
2157    fn overwrite_requires_force_and_preserves_existing_content() {
2158        let directory = TestDirectory::new("overwrite");
2159        let target = find_target("legacy-keccak-round").expect("registered target");
2160        let provenance = fixture_provenance();
2161        let artifact =
2162            generate_target(target, &directory.0, false, &provenance).expect("initial generation");
2163        let original = fs::read(&artifact.verilog_path).expect("read initial output");
2164        let error = generate_target(target, &directory.0, false, &provenance)
2165            .expect_err("overwrite must fail");
2166        assert!(error.to_string().contains("refusing to overwrite"));
2167        assert_eq!(
2168            fs::read(&artifact.verilog_path).expect("read preserved output"),
2169            original
2170        );
2171        generate_target(target, &directory.0, true, &provenance).expect("forced generation");
2172        assert_eq!(
2173            fs::read(&artifact.verilog_path).expect("read forced output"),
2174            original
2175        );
2176    }
2177
2178    #[test]
2179    fn throughput_output_requires_a_new_directory_outside_source_evidence() {
2180        let source = TestDirectory::new("throughput-source-evidence");
2181        let output_parent = TestDirectory::new("throughput-output-parent");
2182        assert!(
2183            create_throughput_output_directory(&source.0, &source.0).is_err(),
2184            "an existing directory must never be reused"
2185        );
2186        assert!(
2187            create_throughput_output_directory(&source.0.join("nested"), &source.0).is_err(),
2188            "the immutable source-evidence directory must never receive output"
2189        );
2190        let selected = output_parent.0.join("new-evidence");
2191        let created = create_throughput_output_directory(&selected, &source.0)
2192            .expect("create new external throughput output");
2193        assert!(created.is_dir());
2194        assert_eq!(
2195            created,
2196            fs::canonicalize(&selected).expect("canonical new output")
2197        );
2198    }
2199
2200    #[test]
2201    fn default_generation_targets_contain_their_declared_top_modules() {
2202        for target in TARGETS
2203            .iter()
2204            .filter(|target| target.default_unit_generation)
2205        {
2206            let generated = (target.generate)().expect("target lowers");
2207            assert!(
2208                generated.contains(&format!("module {}", target.top_module)),
2209                "target {} lacks top {}",
2210                target.name,
2211                target.top_module
2212            );
2213        }
2214    }
2215
2216    #[test]
2217    fn slow_and_unchecked_targets_are_not_in_default_generation_loops() {
2218        for name in [
2219            "sha256-compression-oracle",
2220            "sha256-pipeline-64-unchecked",
2221            "sha256-compression-lane-64-unchecked",
2222            "sha256-compression-farm-3x64-unchecked",
2223        ] {
2224            assert!(
2225                !find_target(name)
2226                    .expect("special target is registered")
2227                    .default_unit_generation
2228            );
2229        }
2230        assert!(
2231            find_target("sha256-pipeline-64-unchecked")
2232                .expect("unchecked target is registered")
2233                .validation_scope
2234                .contains("not RHDL/RTL equivalence evidence")
2235        );
2236    }
2237
2238    #[test]
2239    fn full_check_support_files_make_their_scope_and_lifetime_explicit() {
2240        assert!(full_sha_check::lane_testbench().contains(full_sha_check::LANE_PASS_MARKER));
2241        assert!(full_sha_check::farm_testbench().contains(full_sha_check::FARM_PASS_MARKER));
2242        assert!(
2243            render_artifact_location(Path::new("example.log"), false)
2244                .contains("ephemeral; removed when the check exits")
2245        );
2246        assert!(
2247            render_artifact_location(Path::new("example.log"), true)
2248                .contains("preserved by --keep")
2249        );
2250    }
2251
2252    #[test]
2253    fn verilator_warning_inventory_is_code_specific_and_deterministic() {
2254        let stderr = "%Warning-UNUSEDSIGNAL: first\n%Warning-UNOPTFLAT: second\ncontinuation\n%Warning-UNUSEDSIGNAL: third\n";
2255        let inventory = verilator_warning_inventory(stderr);
2256        assert_eq!(inventory.get("UNOPTFLAT"), Some(&1));
2257        assert_eq!(inventory.get("UNUSEDSIGNAL"), Some(&2));
2258        assert_eq!(
2259            render_warning_inventory(&inventory),
2260            "UNOPTFLAT=1, UNUSEDSIGNAL=2"
2261        );
2262        assert_eq!(render_warning_inventory(&BTreeMap::new()), "none");
2263    }
2264}