Skip to main content

celox_napi/
lib.rs

1mod layout;
2
3use fxhash::FxHashMap as HashMap;
4#[cfg(not(target_arch = "wasm32"))]
5use std::sync::{Arc, Mutex};
6
7use napi::bindgen_prelude::*;
8use napi_derive::napi;
9use veryl_analyzer::{Analyzer, Context, attribute_table, ir::Ir, symbol_table};
10use veryl_metadata::Metadata;
11use veryl_parser::Parser;
12use veryl_path::PathSet;
13
14#[cfg(not(target_arch = "wasm32"))]
15use celox::SimBackend;
16#[cfg(not(target_arch = "wasm32"))]
17use layout::{build_event_map, build_hierarchy_node, build_signal_layout};
18
19/// A segment of a hierarchical instance path.
20#[napi(object)]
21pub struct NapiInstanceSegment {
22    pub name: String,
23    pub index: u32,
24}
25
26/// A signal path consisting of an instance path and a variable path.
27#[napi(object)]
28pub struct NapiSignalPath {
29    pub instance_path: Vec<NapiInstanceSegment>,
30    pub var_path: Vec<String>,
31}
32
33/// A false-loop declaration (combinational loop to ignore).
34#[napi(object)]
35pub struct NapiFalseLoop {
36    pub from: NapiSignalPath,
37    pub to: NapiSignalPath,
38}
39
40/// A true-loop declaration with a convergence iteration limit.
41#[napi(object)]
42pub struct NapiTrueLoop {
43    pub from: NapiSignalPath,
44    pub to: NapiSignalPath,
45    pub max_iter: u32,
46}
47
48/// A source file with its content and path.
49#[napi(object)]
50pub struct NapiSourceFile {
51    pub content: String,
52    pub path: String,
53}
54
55/// A parameter override for a top-level module parameter.
56#[napi(object)]
57pub struct NapiParamOverride {
58    pub name: String,
59    pub value: i64,
60}
61
62/// Per-pass optimizer control. All fields default to true when omitted.
63#[napi(object)]
64pub struct NapiOptimizeOptions {
65    pub store_load_forwarding: Option<bool>,
66    pub hoist_common_branch_loads: Option<bool>,
67    pub bit_extract_peephole: Option<bool>,
68    pub optimize_blocks: Option<bool>,
69    pub split_wide_commits: Option<bool>,
70    pub commit_sinking: Option<bool>,
71    pub inline_commit_forwarding: Option<bool>,
72    pub eliminate_dead_working_stores: Option<bool>,
73    pub reschedule: Option<bool>,
74    pub coalesce_stores: Option<bool>,
75}
76
77/// Options for creating a simulator/simulation handle.
78#[napi(object)]
79pub struct NapiOptions {
80    pub four_state: Option<bool>,
81    pub vcd: Option<String>,
82    /// Optimization level preset: "O0", "O1", or "O2".
83    /// Takes precedence over `optimize` and `optimize_options`.
84    pub opt_level: Option<String>,
85    /// Per-pass overrides applied on top of opt_level.
86    /// Each entry: "+sir:<pass_name>" to enable, "-sir:<pass_name>" to disable.
87    pub pass_overrides: Option<Vec<String>>,
88    /// Shorthand to enable/disable all SIRT optimization passes.
89    /// `true` = all on, `false` = all off. Overridden by `opt_level` or `optimize_options`.
90    pub optimize: Option<bool>,
91    /// Per-pass optimizer flags (legacy). Overridden by `opt_level`/`pass_overrides`.
92    pub optimize_options: Option<NapiOptimizeOptions>,
93    /// Cranelift backend optimization level: "none", "speed", or "speed_and_size".
94    pub cranelift_opt_level: Option<String>,
95    /// Register allocator algorithm: "backtracking" or "single_pass".
96    pub regalloc_algorithm: Option<String>,
97    /// Enable alias analysis in the Cranelift egraph pass. Default: true.
98    pub enable_alias_analysis: Option<bool>,
99    /// Enable the Cranelift IR verifier. Default: true.
100    pub enable_verifier: Option<bool>,
101    pub false_loops: Option<Vec<NapiFalseLoop>>,
102    pub true_loops: Option<Vec<NapiTrueLoop>>,
103    /// Clock polarity: "posedge" or "negedge".
104    pub clock_type: Option<String>,
105    /// Reset type: "async_high", "async_low", "sync_high", or "sync_low".
106    pub reset_type: Option<String>,
107    /// Additional Veryl source to append to the main source code.
108    pub extra_source: Option<String>,
109    /// Parameter overrides for the top-level module.
110    pub parameters: Option<Vec<NapiParamOverride>>,
111    /// Dead store elimination policy: "off", "preserve_top_ports", or "preserve_all_ports".
112    pub dead_store_policy: Option<String>,
113}
114
115/// Parsed builder options from NapiOptions (common fields available on all targets).
116#[allow(dead_code)]
117struct ParsedOptionsCommon {
118    four_state: bool,
119    optimize_options: celox::OptimizeOptions,
120    vcd: Option<String>,
121    false_loops: Vec<(
122        (Vec<(String, usize)>, Vec<String>),
123        (Vec<(String, usize)>, Vec<String>),
124    )>,
125    true_loops: Vec<(
126        (Vec<(String, usize)>, Vec<String>),
127        (Vec<(String, usize)>, Vec<String>),
128        usize,
129    )>,
130    clock_type: Option<celox::ClockType>,
131    reset_type: Option<celox::ResetType>,
132    extra_source: Option<String>,
133    parameters: Vec<(String, u64)>,
134}
135
136/// Parsed builder options from NapiOptions (native-only, includes Cranelift/DSE options).
137#[cfg(not(target_arch = "wasm32"))]
138struct ParsedOptions {
139    common: ParsedOptionsCommon,
140    cranelift_options: celox::CraneliftOptions,
141    dead_store_policy: celox::DeadStorePolicy,
142}
143
144#[cfg(not(target_arch = "wasm32"))]
145impl std::ops::Deref for ParsedOptions {
146    type Target = ParsedOptionsCommon;
147    fn deref(&self) -> &Self::Target {
148        &self.common
149    }
150}
151
152/// Convert a NapiSignalPath to the Rust builder's tuple format.
153fn convert_signal_path(p: &NapiSignalPath) -> (Vec<(String, usize)>, Vec<String>) {
154    let inst: Vec<(String, usize)> = p
155        .instance_path
156        .iter()
157        .map(|seg| (seg.name.clone(), seg.index as usize))
158        .collect();
159    let var_path: Vec<String> = p.var_path.clone();
160    (inst, var_path)
161}
162
163/// Parse a clock type string into ClockType.
164fn parse_clock_type(s: &str) -> Result<celox::ClockType> {
165    match s {
166        "posedge" => Ok(celox::ClockType::PosEdge),
167        "negedge" => Ok(celox::ClockType::NegEdge),
168        _ => Err(Error::from_reason(format!(
169            "Invalid clock_type '{}'. Expected 'posedge' or 'negedge'.",
170            s
171        ))),
172    }
173}
174
175/// Parse a reset type string into ResetType.
176fn parse_reset_type(s: &str) -> Result<celox::ResetType> {
177    match s {
178        "async_high" => Ok(celox::ResetType::AsyncHigh),
179        "async_low" => Ok(celox::ResetType::AsyncLow),
180        "sync_high" => Ok(celox::ResetType::SyncHigh),
181        "sync_low" => Ok(celox::ResetType::SyncLow),
182        _ => Err(Error::from_reason(format!(
183            "Invalid reset_type '{}'. Expected 'async_high', 'async_low', 'sync_high', or 'sync_low'.",
184            s
185        ))),
186    }
187}
188
189/// Convert legacy NapiOptimizeOptions to celox::OptimizeOptions.
190/// Starts from O1 (all on) and disables any explicitly false fields.
191fn convert_optimize_options(napi: &NapiOptimizeOptions) -> celox::OptimizeOptions {
192    let mut opts = celox::OptimizeOptions::all();
193    let fields: &[(Option<bool>, celox::SirPass)] = &[
194        (
195            napi.store_load_forwarding,
196            celox::SirPass::StoreLoadForwarding,
197        ),
198        (
199            napi.hoist_common_branch_loads,
200            celox::SirPass::HoistCommonBranchLoads,
201        ),
202        (
203            napi.bit_extract_peephole,
204            celox::SirPass::BitExtractPeephole,
205        ),
206        (napi.optimize_blocks, celox::SirPass::OptimizeBlocks),
207        (napi.split_wide_commits, celox::SirPass::SplitWideCommits),
208        (napi.commit_sinking, celox::SirPass::CommitSinking),
209        (
210            napi.inline_commit_forwarding,
211            celox::SirPass::InlineCommitForwarding,
212        ),
213        (
214            napi.eliminate_dead_working_stores,
215            celox::SirPass::EliminateDeadWorkingStores,
216        ),
217        (napi.reschedule, celox::SirPass::Reschedule),
218        (napi.coalesce_stores, celox::SirPass::CoalesceStores),
219    ];
220    for &(val, pass) in fields {
221        if let Some(false) = val {
222            opts = opts.disable(pass);
223        }
224    }
225    opts
226}
227
228/// Parse pass override strings like "+sir:reschedule" or "-sir:coalesce_stores".
229fn apply_pass_overrides(
230    mut opts: celox::OptimizeOptions,
231    overrides: &[String],
232) -> Result<celox::OptimizeOptions> {
233    for s in overrides {
234        let (enable, rest) = if let Some(rest) = s.strip_prefix('+') {
235            (true, rest)
236        } else if let Some(rest) = s.strip_prefix('-') {
237            (false, rest)
238        } else {
239            return Err(Error::from_reason(format!(
240                "Invalid pass override '{}'. Must start with '+' or '-'.",
241                s
242            )));
243        };
244        let pass_name = rest.strip_prefix("sir:").unwrap_or(rest);
245        let pass = celox::SirPass::parse(pass_name).ok_or_else(|| {
246            Error::from_reason(format!(
247                "Unknown SIR pass '{}'. Valid passes: {}",
248                pass_name,
249                celox::SirPass::ALL
250                    .iter()
251                    .map(|p| p.as_str())
252                    .collect::<Vec<_>>()
253                    .join(", ")
254            ))
255        })?;
256        opts = if enable {
257            opts.enable(pass)
258        } else {
259            opts.disable(pass)
260        };
261    }
262    Ok(opts)
263}
264
265/// Parse a Cranelift optimization level string.
266#[cfg(not(target_arch = "wasm32"))]
267fn parse_cranelift_opt_level(s: &str) -> Result<celox::CraneliftOptLevel> {
268    match s {
269        "none" => Ok(celox::CraneliftOptLevel::None),
270        "speed" => Ok(celox::CraneliftOptLevel::Speed),
271        "speed_and_size" => Ok(celox::CraneliftOptLevel::SpeedAndSize),
272        _ => Err(Error::from_reason(format!(
273            "Invalid cranelift_opt_level '{}'. Expected 'none', 'speed', or 'speed_and_size'.",
274            s
275        ))),
276    }
277}
278
279/// Parse a register allocator algorithm string.
280#[cfg(not(target_arch = "wasm32"))]
281fn parse_regalloc_algorithm(s: &str) -> Result<celox::RegallocAlgorithm> {
282    match s {
283        "backtracking" => Ok(celox::RegallocAlgorithm::Backtracking),
284        "single_pass" => Ok(celox::RegallocAlgorithm::SinglePass),
285        _ => Err(Error::from_reason(format!(
286            "Invalid regalloc_algorithm '{}'. Expected 'backtracking' or 'single_pass'.",
287            s
288        ))),
289    }
290}
291
292/// Parse a dead store policy string into DeadStorePolicy.
293#[cfg(not(target_arch = "wasm32"))]
294fn parse_dead_store_policy(s: &str) -> Result<celox::DeadStorePolicy> {
295    match s {
296        "off" => Ok(celox::DeadStorePolicy::Off),
297        "preserve_top_ports" => Ok(celox::DeadStorePolicy::PreserveTopPorts),
298        "preserve_all_ports" => Ok(celox::DeadStorePolicy::PreserveAllPorts),
299        _ => Err(Error::from_reason(format!(
300            "Invalid dead_store_policy '{}'. Expected 'off', 'preserve_top_ports', or 'preserve_all_ports'.",
301            s
302        ))),
303    }
304}
305
306/// Helper to extract the common builder config from NapiOptions (available on all targets).
307fn parse_options_common(options: &Option<NapiOptions>) -> Result<ParsedOptionsCommon> {
308    match options.as_ref() {
309        Some(o) => {
310            let false_loops = o
311                .false_loops
312                .as_ref()
313                .map(|loops| {
314                    loops
315                        .iter()
316                        .map(|fl| (convert_signal_path(&fl.from), convert_signal_path(&fl.to)))
317                        .collect()
318                })
319                .unwrap_or_default();
320            let true_loops = o
321                .true_loops
322                .as_ref()
323                .map(|loops| {
324                    loops
325                        .iter()
326                        .map(|tl| {
327                            (
328                                convert_signal_path(&tl.from),
329                                convert_signal_path(&tl.to),
330                                tl.max_iter as usize,
331                            )
332                        })
333                        .collect()
334                })
335                .unwrap_or_default();
336            let clock_type = o.clock_type.as_deref().map(parse_clock_type).transpose()?;
337            let reset_type = o.reset_type.as_deref().map(parse_reset_type).transpose()?;
338            let parameters = o
339                .parameters
340                .as_ref()
341                .map(|params| {
342                    params
343                        .iter()
344                        .map(|p| (p.name.clone(), p.value as u64))
345                        .collect()
346                })
347                .unwrap_or_default();
348            // Resolve optimize_options with priority:
349            // 1. opt_level + pass_overrides (new API)
350            // 2. optimize_options (legacy per-pass bools)
351            // 3. optimize shorthand (legacy bool)
352            // 4. default (O1 = all on)
353            let optimize_options = if let Some(ref level_str) = o.opt_level {
354                let level = celox::OptLevel::parse(level_str).ok_or_else(|| {
355                    Error::from_reason(format!(
356                        "Invalid opt_level '{}'. Expected 'O0', 'O1', or 'O2'.",
357                        level_str
358                    ))
359                })?;
360                let opts = celox::OptimizeOptions::new(level);
361                if let Some(ref overrides) = o.pass_overrides {
362                    apply_pass_overrides(opts, overrides)?
363                } else {
364                    opts
365                }
366            } else if let Some(ref oo) = o.optimize_options {
367                let opts = convert_optimize_options(oo);
368                if let Some(ref overrides) = o.pass_overrides {
369                    apply_pass_overrides(opts, overrides)?
370                } else {
371                    opts
372                }
373            } else if let Some(false) = o.optimize {
374                celox::OptimizeOptions::none()
375            } else {
376                celox::OptimizeOptions::all()
377            };
378            Ok(ParsedOptionsCommon {
379                four_state: o.four_state.unwrap_or(false),
380                optimize_options,
381                vcd: o.vcd.clone(),
382                false_loops,
383                true_loops,
384                clock_type,
385                reset_type,
386                extra_source: o.extra_source.clone(),
387                parameters,
388            })
389        }
390        None => Ok(ParsedOptionsCommon {
391            four_state: false,
392            optimize_options: celox::OptimizeOptions::all(),
393            vcd: None,
394            false_loops: Vec::new(),
395            true_loops: Vec::new(),
396            clock_type: None,
397            reset_type: None,
398            extra_source: None,
399            parameters: Vec::new(),
400        }),
401    }
402}
403
404/// Helper to extract the full builder config from NapiOptions (native only).
405#[cfg(not(target_arch = "wasm32"))]
406fn parse_options(options: &Option<NapiOptions>) -> Result<ParsedOptions> {
407    let common = parse_options_common(options)?;
408    match options.as_ref() {
409        Some(o) => {
410            let dead_store_policy = o
411                .dead_store_policy
412                .as_deref()
413                .map(parse_dead_store_policy)
414                .transpose()?
415                .unwrap_or(celox::DeadStorePolicy::Off);
416            let cranelift_opt_level = o
417                .cranelift_opt_level
418                .as_deref()
419                .map(parse_cranelift_opt_level)
420                .transpose()?
421                .unwrap_or(celox::CraneliftOptLevel::Speed);
422            let regalloc_algorithm = o
423                .regalloc_algorithm
424                .as_deref()
425                .map(parse_regalloc_algorithm)
426                .transpose()?
427                .unwrap_or(celox::RegallocAlgorithm::Backtracking);
428            let cranelift_options = celox::CraneliftOptions {
429                opt_level: cranelift_opt_level,
430                regalloc_algorithm,
431                enable_alias_analysis: o.enable_alias_analysis.unwrap_or(true),
432                enable_verifier: o.enable_verifier.unwrap_or(true),
433                tail_call_split: true,
434                diagnostics: celox::CraneliftDiagnostics::default(),
435            };
436            Ok(ParsedOptions {
437                common,
438                cranelift_options,
439                dead_store_policy,
440            })
441        }
442        None => Ok(ParsedOptions {
443            common,
444            cranelift_options: celox::CraneliftOptions::default(),
445            dead_store_policy: celox::DeadStorePolicy::Off,
446        }),
447    }
448}
449
450/// Append extra source as a separate file entry if provided.
451fn append_extra_source(sources: &mut Vec<(String, std::path::PathBuf)>, extra: &Option<String>) {
452    if let Some(extra) = extra {
453        sources.push((extra.clone(), std::path::PathBuf::from("<extra>")));
454    }
455}
456
457/// Configuration loaded from an optional `celox.toml` in the project root.
458#[derive(serde::Deserialize, Default)]
459#[allow(dead_code)]
460struct CeloxConfig {
461    /// Glob patterns (relative to project root) for `.veryl` files to exclude
462    /// from compilation and type generation.
463    #[serde(default)]
464    exclude: Vec<String>,
465    #[serde(default)]
466    test: CeloxTestConfig,
467    #[serde(default)]
468    simulation: CeloxSimulationConfig,
469}
470
471#[derive(serde::Deserialize, Default)]
472struct CeloxTestConfig {
473    /// Additional source directories (relative to `celox.toml`) whose `.veryl`
474    /// files are included when running simulations and generating type stubs.
475    #[serde(default)]
476    sources: Vec<String>,
477}
478
479#[derive(serde::Deserialize, Default)]
480#[allow(dead_code)]
481struct CeloxSimulationConfig {
482    /// Default maximum steps for `waitUntil` / `waitForCycles`.
483    /// Overridden by the per-call `maxSteps` option.
484    max_steps: Option<u32>,
485}
486
487/// Load `celox.toml` from the given project root (same directory as `Veryl.toml`).
488/// Returns `None` if the file does not exist.
489fn load_celox_config(project_root: &std::path::Path) -> Result<CeloxConfig> {
490    let path = project_root.join("celox.toml");
491    if !path.exists() {
492        return Ok(CeloxConfig::default());
493    }
494    let content = std::fs::read_to_string(&path)
495        .map_err(|e| Error::from_reason(format!("Failed to read celox.toml: {e}")))?;
496    toml::from_str(&content)
497        .map_err(|e| Error::from_reason(format!("Failed to parse celox.toml: {e}")))
498}
499
500/// Build a `GlobSet` from the exclude patterns in the config.
501/// Returns `None` if there are no exclude patterns.
502fn build_exclude_set(config: &CeloxConfig) -> Result<Option<globset::GlobSet>> {
503    if config.exclude.is_empty() {
504        return Ok(None);
505    }
506    let mut builder = globset::GlobSetBuilder::new();
507    for pattern in &config.exclude {
508        let glob = globset::GlobBuilder::new(pattern)
509            .literal_separator(true)
510            .build()
511            .map_err(|e| Error::from_reason(format!("Invalid exclude pattern '{pattern}': {e}")))?;
512        builder.add(glob);
513    }
514    let set = builder
515        .build()
516        .map_err(|e| Error::from_reason(format!("Failed to build exclude set: {e}")))?;
517    Ok(Some(set))
518}
519
520/// Returns `true` if the path should be excluded based on the glob set.
521/// The path is matched relative to `project_root`.
522fn is_excluded(
523    path: &std::path::Path,
524    project_root: &std::path::Path,
525    exclude_set: &globset::GlobSet,
526) -> bool {
527    let relative = path.strip_prefix(project_root).unwrap_or(path);
528    // Normalize to forward slashes for consistent matching
529    let rel_str = relative.to_string_lossy().replace('\\', "/");
530    exclude_set.is_match(&rel_str)
531}
532
533/// Collect all `.veryl` files from the extra test source directories declared in
534/// `celox.toml` and add them as individual source entries.
535fn collect_test_sources(
536    sources: &mut Vec<(String, std::path::PathBuf)>,
537    project_root: &std::path::Path,
538    config: &CeloxConfig,
539) -> Result<()> {
540    for dir in &config.test.sources {
541        let dir_path = project_root.join(dir);
542        if !dir_path.exists() {
543            continue;
544        }
545        let entries = walkdir(&dir_path)?;
546        for entry in entries {
547            let content = std::fs::read_to_string(&entry)
548                .map_err(|e| Error::from_reason(format!("{}: {e}", entry.display())))?;
549            sources.push((content, entry));
550        }
551    }
552    Ok(())
553}
554
555/// Recursively collect `.veryl` files under `dir`, sorted for determinism.
556fn walkdir(dir: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
557    let mut files = Vec::new();
558    let read = std::fs::read_dir(dir)
559        .map_err(|e| Error::from_reason(format!("Cannot read directory {}: {e}", dir.display())))?;
560    for entry in read {
561        let entry = entry.map_err(|e| Error::from_reason(format!("Directory entry error: {e}")))?;
562        let path = entry.path();
563        if path.is_dir() {
564            files.extend(walkdir(&path)?);
565        } else if path.extension().is_some_and(|ext| ext == "veryl") {
566            files.push(path);
567        }
568    }
569    files.sort();
570    Ok(files)
571}
572
573/// Load a Veryl project's source files and metadata from a directory.
574///
575/// Searches upward from `project_path` for `Veryl.toml`, gathers all `.veryl`
576/// source files, and returns the per-file sources, project metadata, and
577/// the parsed `celox.toml` configuration.
578fn load_project_sources(
579    project_path: &str,
580) -> Result<(Vec<(String, std::path::PathBuf)>, Metadata, CeloxConfig)> {
581    let toml_path = Metadata::search_from(project_path)
582        .map_err(|e| Error::from_reason(format!("Could not find Veryl.toml: {e}")))?;
583    let mut metadata = Metadata::load(&toml_path)
584        .map_err(|e| Error::from_reason(format!("Failed to load Veryl.toml: {e}")))?;
585    let paths = metadata
586        .paths::<&str>(&[], false, false)
587        .map_err(|e| Error::from_reason(format!("Failed to gather sources: {e}")))?;
588    let mut sources = Vec::new();
589    for p in paths.iter().filter(|path| !path.example) {
590        let content = std::fs::read_to_string(&p.src)
591            .map_err(|e| Error::from_reason(format!("{}: {e}", p.src.display())))?;
592        sources.push((content, p.src.clone()));
593    }
594    let project_root = toml_path.parent().unwrap_or(&toml_path);
595    let celox_cfg = load_celox_config(project_root)?;
596    collect_test_sources(&mut sources, project_root, &celox_cfg)?;
597    if let Some(exclude_set) = build_exclude_set(&celox_cfg)? {
598        sources.retain(|(_, path)| !is_excluded(path, project_root, &exclude_set));
599    }
600    Ok((sources, metadata, celox_cfg))
601}
602
603/// Format compilation warnings as a JSON array of strings.
604///
605/// Uses `render_diagnostic` to include source location and span information,
606/// matching the format used for error messages.
607fn format_warnings_json(warnings: &[celox::CompilationWarning]) -> String {
608    let msgs: Vec<String> = warnings
609        .iter()
610        .map(|w| celox::render_diagnostic(w))
611        .collect();
612    serde_json::to_string(&msgs).unwrap_or_else(|_| "[]".to_string())
613}
614
615/// Apply parsed options to a SimulatorBuilder.
616#[cfg(not(target_arch = "wasm32"))]
617fn apply_options<'a, T>(
618    mut builder: celox::SimulatorBuilder<'a, T>,
619    opts: &ParsedOptions,
620) -> celox::SimulatorBuilder<'a, T> {
621    builder = builder.four_state(opts.four_state);
622    builder = builder.optimize_options(opts.optimize_options.clone());
623    builder = builder.cranelift_options(opts.cranelift_options);
624    // VCD is handled separately after build — not passed to SimulatorBuilder
625    for (from, to) in &opts.false_loops {
626        builder = builder.false_loop(from.clone(), to.clone());
627    }
628    for (from, to, max_iter) in &opts.true_loops {
629        builder = builder.true_loop(from.clone(), to.clone(), *max_iter);
630    }
631    if let Some(ct) = opts.clock_type {
632        builder = builder.clock_type(ct);
633    }
634    if let Some(rt) = opts.reset_type {
635        builder = builder.reset_type(rt);
636    }
637    for (name, value) in &opts.parameters {
638        builder = builder.param(name, *value);
639    }
640    builder = builder.dead_store_policy(opts.dead_store_policy);
641    builder
642}
643
644// ---------------------------------------------------------------------------
645//  Process-global compiled-code cache (native only)
646// ---------------------------------------------------------------------------
647
648#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
649type SharedCode = celox::SharedNativeCode;
650#[cfg(all(
651    not(target_arch = "wasm32"),
652    not(any(target_arch = "x86_64", target_arch = "aarch64"))
653))]
654type SharedCode = celox::SharedJitCode;
655
656#[cfg(not(target_arch = "wasm32"))]
657/// Cached compilation result shared across simulator instances.
658struct CachedBuild {
659    shared_code: Arc<SharedCode>,
660    runtime_errors: HashMap<i64, (String, Vec<String>)>,
661    layout_json: String,
662    events_json: String,
663    hierarchy_json: String,
664    warnings_json: String,
665    stable_size: u32,
666    total_size: u32,
667    /// Pre-computed VCD signal descriptors so VCD works on cache hits.
668    vcd_descs: Vec<celox::VcdSignalDesc>,
669}
670
671#[cfg(not(target_arch = "wasm32"))]
672/// Exact cache key — no hashing, no collisions.
673///
674/// Contains the full source content + paths + top module + all compilation-
675/// affecting options. Two builds produce the same `CacheKey` iff they would
676/// produce identical compiled code.
677#[derive(Clone, Debug, PartialEq, Eq, Hash)]
678struct CacheKey {
679    /// (path, content) sorted by path for determinism.
680    sources: Vec<(String, String)>,
681    top: String,
682    four_state: bool,
683    sir_optimization: SirOptimizationCacheKey,
684    cranelift_opt_level: u8,
685    regalloc_algorithm: u8,
686    enable_alias_analysis: bool,
687    enable_verifier: bool,
688    dead_store_policy: u8,
689    clock_type: Option<u8>,
690    reset_type: Option<u8>,
691    parameters: Vec<(String, u64)>,
692    false_loops: Vec<(
693        (Vec<(String, usize)>, Vec<String>),
694        (Vec<(String, usize)>, Vec<String>),
695    )>,
696    true_loops: Vec<(
697        (Vec<(String, usize)>, Vec<String>),
698        (Vec<(String, usize)>, Vec<String>),
699        usize,
700    )>,
701    /// Effective clock/reset from metadata (from_project path).
702    /// None when using the `new` constructor (no metadata).
703    metadata_clock_type: Option<u8>,
704    metadata_reset_type: Option<u8>,
705}
706
707/// Collision-free representation of every SIR code-generation option.
708#[cfg(not(target_arch = "wasm32"))]
709#[derive(Clone, Debug, PartialEq, Eq, Hash)]
710struct SirOptimizationCacheKey {
711    opt_level: celox::OptLevel,
712    enabled_passes: Box<[bool]>,
713    max_native_memory_width: usize,
714}
715
716#[cfg(not(target_arch = "wasm32"))]
717impl From<&celox::OptimizeOptions> for SirOptimizationCacheKey {
718    fn from(options: &celox::OptimizeOptions) -> Self {
719        Self {
720            opt_level: options.opt_level(),
721            enabled_passes: celox::SirPass::ALL
722                .iter()
723                .map(|&pass| options.is_enabled(pass))
724                .collect(),
725            max_native_memory_width: options.max_native_memory_width(),
726        }
727    }
728}
729
730#[cfg(not(target_arch = "wasm32"))]
731static JIT_CACHE: std::sync::LazyLock<Mutex<HashMap<CacheKey, Arc<CachedBuild>>>> =
732    std::sync::LazyLock::new(|| Mutex::new(HashMap::default()));
733
734#[cfg(not(target_arch = "wasm32"))]
735/// Build a collision-free cache key from source content, top module, and options.
736///
737/// When `metadata` is `Some`, the effective clock/reset settings from
738/// `Veryl.toml` are included in the key so that changing project config
739/// invalidates the cache.
740fn build_cache_key(
741    sources: &[(String, std::path::PathBuf)],
742    top: &str,
743    opts: &ParsedOptions,
744    metadata: Option<&Metadata>,
745) -> CacheKey {
746    let mut sorted_sources: Vec<(String, String)> = sources
747        .iter()
748        .map(|(content, path)| (path.to_string_lossy().into_owned(), content.clone()))
749        .collect();
750    sorted_sources.sort_by(|a, b| a.0.cmp(&b.0));
751
752    CacheKey {
753        sources: sorted_sources,
754        top: top.to_string(),
755        four_state: opts.four_state,
756        sir_optimization: SirOptimizationCacheKey::from(&opts.optimize_options),
757        cranelift_opt_level: opts.cranelift_options.opt_level as u8,
758        regalloc_algorithm: opts.cranelift_options.regalloc_algorithm as u8,
759        enable_alias_analysis: opts.cranelift_options.enable_alias_analysis,
760        enable_verifier: opts.cranelift_options.enable_verifier,
761        dead_store_policy: opts.dead_store_policy as u8,
762        clock_type: opts.clock_type.map(|ct| ct as u8),
763        reset_type: opts.reset_type.map(|rt| rt as u8),
764        parameters: opts.parameters.clone(),
765        false_loops: opts.false_loops.clone(),
766        true_loops: opts.true_loops.clone(),
767        metadata_clock_type: metadata.map(|m| m.build.clock_type as u8),
768        metadata_reset_type: metadata.map(|m| m.build.reset_type as u8),
769    }
770}
771
772#[cfg(not(target_arch = "wasm32"))]
773fn runtime_errors_by_name(program: &celox::RuntimeProgram) -> HashMap<i64, (String, Vec<String>)> {
774    program
775        .runtime_schema
776        .runtime_errors
777        .iter()
778        .map(|(&code, info)| {
779            (
780                code,
781                (
782                    info.message.clone(),
783                    info.signals
784                        .iter()
785                        .map(|addr| program.get_path(addr))
786                        .collect(),
787                ),
788            )
789        })
790        .collect()
791}
792
793#[cfg(not(target_arch = "wasm32"))]
794fn napi_runtime_error(
795    runtime_errors: &HashMap<i64, (String, Vec<String>)>,
796    err: celox::RuntimeErrorCode,
797) -> Error {
798    match err {
799        celox::RuntimeErrorCode::DetectedTrueLoopCode(code) => {
800            if let Some((message, signals)) = runtime_errors.get(&code) {
801                if message == "Detected True Loop" {
802                    Error::from_reason(format!(
803                        "{}",
804                        celox::RuntimeErrorCode::DetectedTrueLoopAt {
805                            signals: signals.clone(),
806                        }
807                    ))
808                } else {
809                    Error::from_reason(format!(
810                        "{}",
811                        celox::RuntimeErrorCode::Runtime {
812                            message: message.clone(),
813                            signals: signals.clone(),
814                        }
815                    ))
816                }
817            } else {
818                Error::from_reason(format!("{}", celox::RuntimeErrorCode::DetectedTrueLoop))
819            }
820        }
821        other => Error::from_reason(format!("{}", other)),
822    }
823}
824
825/// Low-level handle wrapping the default backend and optional VCD writer.
826///
827/// JS holds this as an opaque class; all operations go through methods.
828#[cfg(not(target_arch = "wasm32"))]
829#[napi]
830pub struct NativeSimulatorHandle {
831    backend: Option<celox::DefaultBackend>,
832    runtime_errors: HashMap<i64, (String, Vec<String>)>,
833    vcd_writer: Option<celox::VcdWriter>,
834    layout_json: String,
835    events_json: String,
836    hierarchy_json: String,
837    warnings_json: String,
838    stable_size: u32,
839    total_size: u32,
840}
841
842#[cfg(not(target_arch = "wasm32"))]
843impl NativeSimulatorHandle {
844    /// Wrap a simulator built by an external frontend in the standard Celox
845    /// N-API handle.
846    ///
847    /// This is a Rust-only adapter API. A frontend binding can accept its own
848    /// artifact type in a `#[napi]` function, lower it with
849    /// `celox-frontend-sdk`, build a [`celox::Simulator`], and pass the value
850    /// here without serializing an artifact to JSON. Signal metadata is always
851    /// derived from the simulator's actual memory layout.
852    pub fn from_simulator(simulator: celox::Simulator, vcd_path: Option<&str>) -> Result<Self> {
853        Self::build_and_cache(simulator, vcd_path, None)
854    }
855
856    /// Build an N-API handle directly from an in-memory frontend artifact.
857    ///
858    /// Frontend bindings normally expose an artifact-specific function such
859    /// as `from_my_artifact` and use this as their final adapter step.
860    pub fn from_frontend(
861        artifact: celox::FrontendArtifact,
862        options: Option<NapiOptions>,
863    ) -> Result<Self> {
864        let opts = parse_options(&options)?;
865        let builder = apply_options(celox::Simulator::from_frontend(artifact), &opts);
866        let simulator = builder
867            .build()
868            .map_err(|error| Error::from_reason(error.to_string()))?;
869        Self::from_simulator(simulator, opts.vcd.as_deref())
870    }
871}
872
873#[cfg(not(target_arch = "wasm32"))]
874#[napi]
875impl NativeSimulatorHandle {
876    /// Build a full simulator, extract metadata, cache the compiled code,
877    /// and return the handle with the default backend (and optional VcdWriter).
878    fn build_and_cache(
879        sim: celox::Simulator,
880        vcd_path: Option<&str>,
881        cache_key: Option<CacheKey>,
882    ) -> Result<Self> {
883        let four_state = sim.layout().four_state;
884        let warnings_json = format_warnings_json(sim.warnings());
885        let signals = sim.named_signals();
886        let events = sim.named_events();
887        let hierarchy = sim.named_hierarchy();
888        let (_, total_size) = sim.memory_as_ptr();
889        let stable_size = sim.stable_region_size();
890        let vcd_descs = sim.build_vcd_descs(four_state);
891        let runtime_errors = runtime_errors_by_name(sim.program());
892
893        let layout_map = build_signal_layout(&signals, four_state);
894        let event_map = build_event_map(&events);
895        let hierarchy_node = build_hierarchy_node(&hierarchy, four_state);
896
897        let layout_json = serde_json::to_string(&layout_map)
898            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
899        let events_json = serde_json::to_string(&event_map)
900            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
901        let hierarchy_json = serde_json::to_string(&hierarchy_node)
902            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
903
904        // Cache the compiled code + metadata for future instances
905        if let Some(key) = cache_key {
906            let cached = Arc::new(CachedBuild {
907                shared_code: sim.shared_code(),
908                runtime_errors: runtime_errors.clone(),
909                layout_json: layout_json.clone(),
910                events_json: events_json.clone(),
911                hierarchy_json: hierarchy_json.clone(),
912                warnings_json: warnings_json.clone(),
913                stable_size: stable_size as u32,
914                total_size: total_size as u32,
915                vcd_descs: vcd_descs.clone(),
916            });
917            let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
918            cache.insert(key, cached);
919        }
920
921        // Create VcdWriter if requested
922        let vcd_writer = if let Some(path) = vcd_path {
923            Some(
924                celox::VcdWriter::new(path, &vcd_descs)
925                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
926            )
927        } else {
928            None
929        };
930
931        // Extract the backend from Simulator (drops runtime metadata which is no longer needed)
932        let backend = sim.into_backend();
933
934        Ok(Self {
935            backend: Some(backend),
936            runtime_errors,
937            vcd_writer,
938            layout_json,
939            events_json,
940            hierarchy_json,
941            warnings_json,
942            stable_size: stable_size as u32,
943            total_size: total_size as u32,
944        })
945    }
946
947    /// Create a handle from a cached build (shared compiled code + fresh memory).
948    fn from_cached(cached: &CachedBuild, vcd_path: Option<&str>) -> Result<Self> {
949        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
950        let backend = celox::NativeBackend::from_shared(Arc::clone(&cached.shared_code));
951        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
952        let backend = celox::JitBackend::from_shared(Arc::clone(&cached.shared_code));
953        let vcd_writer = if let Some(path) = vcd_path {
954            Some(
955                celox::VcdWriter::new(path, &cached.vcd_descs)
956                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
957            )
958        } else {
959            None
960        };
961        Ok(Self {
962            backend: Some(backend),
963            runtime_errors: cached.runtime_errors.clone(),
964            vcd_writer,
965            layout_json: cached.layout_json.clone(),
966            events_json: cached.events_json.clone(),
967            hierarchy_json: cached.hierarchy_json.clone(),
968            warnings_json: cached.warnings_json.clone(),
969            stable_size: cached.stable_size,
970            total_size: cached.total_size,
971        })
972    }
973
974    /// Create a new simulator from Veryl source code.
975    #[napi(constructor)]
976    pub fn new(
977        sources: Vec<NapiSourceFile>,
978        top: String,
979        options: Option<NapiOptions>,
980    ) -> Result<Self> {
981        let opts = parse_options(&options)?;
982        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
983            .into_iter()
984            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
985            .collect();
986        append_extra_source(&mut src_pairs, &opts.extra_source);
987
988        let cache_key = build_cache_key(&src_pairs, &top, &opts, None);
989
990        {
991            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
992            if let Some(cached) = cache.get(&cache_key) {
993                return Self::from_cached(cached, opts.vcd.as_deref());
994            }
995        }
996
997        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
998            .iter()
999            .map(|(s, p)| (s.as_str(), p.as_path()))
1000            .collect();
1001        let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts);
1002        let sim = builder
1003            .build()
1004            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1005
1006        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1007    }
1008
1009    /// Create a simulator from a versioned external-frontend artifact.
1010    #[napi(factory)]
1011    pub fn from_frontend_artifact(
1012        artifact_json: String,
1013        options: Option<NapiOptions>,
1014    ) -> Result<Self> {
1015        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1016            .map_err(|error| Error::from_reason(error.to_string()))?;
1017        Self::from_frontend(artifact, options)
1018    }
1019
1020    /// Create a new simulator from a Veryl project directory.
1021    ///
1022    /// Searches upward from `project_path` for `Veryl.toml`, gathers all
1023    /// `.veryl` source files, and builds the simulator using the project's
1024    /// clock/reset settings.
1025    #[napi(factory)]
1026    pub fn from_project(
1027        project_path: String,
1028        top: String,
1029        options: Option<NapiOptions>,
1030    ) -> Result<Self> {
1031        let opts = parse_options(&options)?;
1032        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1033        append_extra_source(&mut sources, &opts.extra_source);
1034
1035        let cache_key = build_cache_key(&sources, &top, &opts, Some(&metadata));
1036
1037        {
1038            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1039            if let Some(cached) = cache.get(&cache_key) {
1040                return Self::from_cached(cached, opts.vcd.as_deref());
1041            }
1042        }
1043
1044        let source_refs: Vec<(&str, &std::path::Path)> = sources
1045            .iter()
1046            .map(|(s, p)| (s.as_str(), p.as_path()))
1047            .collect();
1048
1049        let builder = apply_options(
1050            celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
1051            &opts,
1052        );
1053        let sim = builder
1054            .build()
1055            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1056
1057        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1058    }
1059
1060    /// Returns the signal layout as a JSON string.
1061    #[napi(getter)]
1062    pub fn layout_json(&self) -> String {
1063        self.layout_json.clone()
1064    }
1065
1066    /// Returns the event map as a JSON string.
1067    #[napi(getter)]
1068    pub fn events_json(&self) -> String {
1069        self.events_json.clone()
1070    }
1071
1072    /// Returns the instance hierarchy as a JSON string.
1073    #[napi(getter)]
1074    pub fn hierarchy_json(&self) -> String {
1075        self.hierarchy_json.clone()
1076    }
1077
1078    /// Returns compilation warnings as a JSON array of strings.
1079    #[napi(getter)]
1080    pub fn warnings_json(&self) -> String {
1081        self.warnings_json.clone()
1082    }
1083
1084    /// Returns the stable region size in bytes.
1085    #[napi(getter)]
1086    pub fn stable_size(&self) -> u32 {
1087        self.stable_size
1088    }
1089
1090    /// Returns the total memory size in bytes.
1091    #[napi(getter)]
1092    pub fn total_size(&self) -> u32 {
1093        self.total_size
1094    }
1095
1096    /// Trigger a clock/event by its numeric ID.
1097    #[napi]
1098    pub fn tick(&mut self, event_id: u32) -> Result<()> {
1099        let runtime_errors = self.runtime_errors.clone();
1100        let b = self
1101            .backend
1102            .as_mut()
1103            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1104        let event = b.id_to_event_slice()[event_id as usize];
1105        b.eval_comb()
1106            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1107        b.eval_apply_ff_at(event)
1108            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1109        b.eval_comb()
1110            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1111    }
1112
1113    /// Trigger a clock/event N times in a single NAPI call.
1114    #[napi]
1115    pub fn tick_n(&mut self, event_id: u32, count: u32) -> Result<()> {
1116        let runtime_errors = self.runtime_errors.clone();
1117        let b = self
1118            .backend
1119            .as_mut()
1120            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1121        let event = b.id_to_event_slice()[event_id as usize];
1122        for _ in 0..count {
1123            b.eval_comb()
1124                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1125            b.eval_apply_ff_at(event)
1126                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1127            b.eval_comb()
1128                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1129        }
1130        Ok(())
1131    }
1132
1133    /// Evaluate combinational logic.
1134    #[napi]
1135    pub fn eval_comb(&mut self) -> Result<()> {
1136        let runtime_errors = self.runtime_errors.clone();
1137        let b = self
1138            .backend
1139            .as_mut()
1140            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1141        b.eval_comb()
1142            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1143    }
1144
1145    /// Write VCD dump at the given timestamp.
1146    #[napi]
1147    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1148        let b = self
1149            .backend
1150            .as_ref()
1151            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1152        if let Some(ref mut writer) = self.vcd_writer {
1153            let (ptr, size) = b.memory_as_ptr();
1154            let memory = unsafe { std::slice::from_raw_parts(ptr, size) };
1155            writer
1156                .dump(timestamp as u64, memory)
1157                .map_err(|e| Error::from_reason(format!("VCD write error: {}", e)))?;
1158        }
1159        Ok(())
1160    }
1161
1162    /// Return the simulator's stable memory region as a zero-copy `Uint8Array`.
1163    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
1164    #[napi]
1165    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
1166        let b = self
1167            .backend
1168            .as_mut()
1169            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1170        let (ptr, _) = b.memory_as_mut_ptr();
1171        let stable_size = b.stable_region_size();
1172        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, |_, _| {}) })
1173    }
1174
1175    /// Invalidate this handle (no-op on the Rust side; drop happens via GC).
1176    #[napi]
1177    pub fn dispose(&mut self) {
1178        self.backend = None;
1179        self.vcd_writer = None;
1180    }
1181}
1182
1183/// Low-level handle wrapping a `celox::Simulation`.
1184#[cfg(not(target_arch = "wasm32"))]
1185#[napi]
1186pub struct NativeSimulationHandle {
1187    sim: Option<celox::Simulation>,
1188    layout_json: String,
1189    events_json: String,
1190    hierarchy_json: String,
1191    warnings_json: String,
1192    stable_size: u32,
1193    total_size: u32,
1194    /// Default `maxSteps` for `waitUntil` / `waitForCycles`, sourced from
1195    /// `[simulation] max_steps` in `celox.toml`. `None` when not set.
1196    default_max_steps: Option<u32>,
1197}
1198
1199#[cfg(not(target_arch = "wasm32"))]
1200#[napi]
1201impl NativeSimulationHandle {
1202    /// Create a new timed simulation from Veryl source code.
1203    #[napi(constructor)]
1204    pub fn new(
1205        sources: Vec<NapiSourceFile>,
1206        top: String,
1207        options: Option<NapiOptions>,
1208    ) -> Result<Self> {
1209        let opts = parse_options(&options)?;
1210        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1211            .into_iter()
1212            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1213            .collect();
1214        append_extra_source(&mut src_pairs, &opts.extra_source);
1215        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1216            .iter()
1217            .map(|(s, p)| (s.as_str(), p.as_path()))
1218            .collect();
1219        let mut builder = apply_options(celox::Simulation::from_sources(source_refs, &top), &opts);
1220        if let Some(path) = &opts.vcd {
1221            builder = builder.vcd(path);
1222        }
1223        let sim = builder
1224            .build()
1225            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1226
1227        let warnings_json = format_warnings_json(sim.warnings());
1228        let signals = sim.named_signals();
1229        let events = sim.named_events();
1230        let hierarchy = sim.named_hierarchy();
1231        let (_, total_size) = sim.memory_as_ptr();
1232        let stable_size = sim.stable_region_size();
1233
1234        let layout_map = build_signal_layout(&signals, opts.four_state);
1235        let event_map = build_event_map(&events);
1236        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1237
1238        let layout_json = serde_json::to_string(&layout_map)
1239            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1240        let events_json = serde_json::to_string(&event_map)
1241            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1242        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1243            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1244
1245        Ok(Self {
1246            sim: Some(sim),
1247            layout_json,
1248            events_json,
1249            hierarchy_json,
1250            warnings_json,
1251            stable_size: stable_size as u32,
1252            total_size: total_size as u32,
1253            default_max_steps: None,
1254        })
1255    }
1256
1257    /// Create a timed simulation from a versioned external-frontend artifact.
1258    #[napi(factory)]
1259    pub fn from_frontend_artifact(
1260        artifact_json: String,
1261        options: Option<NapiOptions>,
1262    ) -> Result<Self> {
1263        let opts = parse_options(&options)?;
1264        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1265            .map_err(|error| Error::from_reason(error.to_string()))?;
1266        let mut builder = apply_options(celox::Simulation::from_frontend(artifact), &opts);
1267        if let Some(path) = &opts.vcd {
1268            builder = builder.vcd(path);
1269        }
1270        let sim = builder
1271            .build()
1272            .map_err(|error| Error::from_reason(error.to_string()))?;
1273
1274        let warnings_json = format_warnings_json(sim.warnings());
1275        let signals = sim.named_signals();
1276        let events = sim.named_events();
1277        let hierarchy = sim.named_hierarchy();
1278        let (_, total_size) = sim.memory_as_ptr();
1279        let stable_size = sim.stable_region_size();
1280        let layout_map = build_signal_layout(&signals, opts.four_state);
1281        let event_map = build_event_map(&events);
1282        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1283        let layout_json = serde_json::to_string(&layout_map)
1284            .map_err(|error| Error::from_reason(format!("Failed to serialize layout: {error}")))?;
1285        let events_json = serde_json::to_string(&event_map)
1286            .map_err(|error| Error::from_reason(format!("Failed to serialize events: {error}")))?;
1287        let hierarchy_json = serde_json::to_string(&hierarchy_node).map_err(|error| {
1288            Error::from_reason(format!("Failed to serialize hierarchy: {error}"))
1289        })?;
1290
1291        Ok(Self {
1292            sim: Some(sim),
1293            layout_json,
1294            events_json,
1295            hierarchy_json,
1296            warnings_json,
1297            stable_size: stable_size as u32,
1298            total_size: total_size as u32,
1299            default_max_steps: None,
1300        })
1301    }
1302
1303    /// Create a new timed simulation from a Veryl project directory.
1304    #[napi(factory)]
1305    pub fn from_project(
1306        project_path: String,
1307        top: String,
1308        options: Option<NapiOptions>,
1309    ) -> Result<Self> {
1310        let opts = parse_options(&options)?;
1311        let (mut sources, metadata, celox_cfg) = load_project_sources(&project_path)?;
1312        append_extra_source(&mut sources, &opts.extra_source);
1313        let source_refs: Vec<(&str, &std::path::Path)> = sources
1314            .iter()
1315            .map(|(s, p)| (s.as_str(), p.as_path()))
1316            .collect();
1317
1318        let mut builder = apply_options(
1319            celox::Simulation::from_sources(source_refs, &top).with_metadata(metadata),
1320            &opts,
1321        );
1322        if let Some(path) = &opts.vcd {
1323            builder = builder.vcd(path);
1324        }
1325        let sim = builder
1326            .build()
1327            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1328
1329        let warnings_json = format_warnings_json(sim.warnings());
1330        let signals = sim.named_signals();
1331        let events = sim.named_events();
1332        let hierarchy = sim.named_hierarchy();
1333        let (_, total_size) = sim.memory_as_ptr();
1334        let stable_size = sim.stable_region_size();
1335
1336        let layout_map = build_signal_layout(&signals, opts.four_state);
1337        let event_map = build_event_map(&events);
1338        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1339
1340        let layout_json = serde_json::to_string(&layout_map)
1341            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1342        let events_json = serde_json::to_string(&event_map)
1343            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1344        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1345            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1346
1347        Ok(Self {
1348            sim: Some(sim),
1349            layout_json,
1350            events_json,
1351            hierarchy_json,
1352            warnings_json,
1353            stable_size: stable_size as u32,
1354            total_size: total_size as u32,
1355            default_max_steps: celox_cfg.simulation.max_steps,
1356        })
1357    }
1358
1359    /// Returns the signal layout as a JSON string.
1360    #[napi(getter)]
1361    pub fn layout_json(&self) -> String {
1362        self.layout_json.clone()
1363    }
1364
1365    /// Returns the event map as a JSON string.
1366    #[napi(getter)]
1367    pub fn events_json(&self) -> String {
1368        self.events_json.clone()
1369    }
1370
1371    /// Returns the instance hierarchy as a JSON string.
1372    #[napi(getter)]
1373    pub fn hierarchy_json(&self) -> String {
1374        self.hierarchy_json.clone()
1375    }
1376
1377    /// Returns compilation warnings as a JSON array of strings.
1378    #[napi(getter)]
1379    pub fn warnings_json(&self) -> String {
1380        self.warnings_json.clone()
1381    }
1382
1383    /// Returns the stable region size in bytes.
1384    #[napi(getter)]
1385    pub fn stable_size(&self) -> u32 {
1386        self.stable_size
1387    }
1388
1389    /// Returns the total memory size in bytes.
1390    #[napi(getter)]
1391    pub fn total_size(&self) -> u32 {
1392        self.total_size
1393    }
1394
1395    /// Returns the default `maxSteps` from `[simulation] max_steps` in `celox.toml`,
1396    /// or `null` if not configured.
1397    #[napi(getter)]
1398    pub fn default_max_steps(&self) -> Option<u32> {
1399        self.default_max_steps
1400    }
1401
1402    /// Register a clock by event ID.
1403    #[napi]
1404    pub fn add_clock(&mut self, event_id: u32, period: f64, initial_delay: f64) -> Result<()> {
1405        let sim = self
1406            .sim
1407            .as_mut()
1408            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1409        sim.add_clock_by_id(event_id, period as u64, initial_delay as u64);
1410        Ok(())
1411    }
1412
1413    /// Schedule a one-shot event by event ID.
1414    #[napi]
1415    pub fn schedule(&mut self, event_id: u32, time: f64, value: f64) -> Result<()> {
1416        let sim = self
1417            .sim
1418            .as_mut()
1419            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1420        sim.schedule_by_id(event_id, time as u64, value as u64)
1421            .map_err(|e| Error::from_reason(format!("{}", e)))
1422    }
1423
1424    /// Advance simulation until `end_time`.
1425    #[napi]
1426    pub fn run_until(&mut self, end_time: f64) -> Result<()> {
1427        let sim = self
1428            .sim
1429            .as_mut()
1430            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1431        sim.run_until(end_time as u64)
1432            .map_err(|e| Error::from_reason(format!("{}", e)))
1433    }
1434
1435    /// Advance to the next event. Returns the new time, or null if no events.
1436    #[napi]
1437    pub fn step(&mut self) -> Result<Option<f64>> {
1438        let sim = self
1439            .sim
1440            .as_mut()
1441            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1442        sim.step()
1443            .map(|opt| opt.map(|t| t as f64))
1444            .map_err(|e| Error::from_reason(format!("{}", e)))
1445    }
1446
1447    /// Returns the current simulation time.
1448    #[napi]
1449    pub fn time(&self) -> Result<f64> {
1450        let sim = self
1451            .sim
1452            .as_ref()
1453            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1454        Ok(sim.time() as f64)
1455    }
1456
1457    /// Returns the time of the next scheduled event, or null if none.
1458    #[napi]
1459    pub fn next_event_time(&self) -> Result<Option<f64>> {
1460        let sim = self
1461            .sim
1462            .as_ref()
1463            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1464        Ok(sim.next_event_time().map(|t| t as f64))
1465    }
1466
1467    /// Evaluate combinational logic.
1468    #[napi]
1469    pub fn eval_comb(&mut self) -> Result<()> {
1470        let sim = self
1471            .sim
1472            .as_mut()
1473            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1474        sim.eval_comb()
1475            .map_err(|e| Error::from_reason(format!("{}", e)))
1476    }
1477
1478    /// Write VCD dump at the given timestamp.
1479    #[napi]
1480    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1481        let sim = self
1482            .sim
1483            .as_mut()
1484            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1485        sim.dump(timestamp as u64);
1486        Ok(())
1487    }
1488
1489    /// Return the simulation's stable memory region as a zero-copy `Uint8Array`.
1490    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
1491    #[napi]
1492    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
1493        let sim = self
1494            .sim
1495            .as_mut()
1496            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1497        let (ptr, _) = sim.memory_as_mut_ptr();
1498        let stable_size = sim.stable_region_size();
1499        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, |_, _| {}) })
1500    }
1501
1502    /// Invalidate this handle.
1503    #[napi]
1504    pub fn dispose(&mut self) {
1505        self.sim = None;
1506    }
1507}
1508
1509// ---------------------------------------------------------------------------
1510//  WASM32 NativeSimulatorHandle — compiles Veryl to WASM bytecode
1511// ---------------------------------------------------------------------------
1512
1513#[cfg(target_arch = "wasm32")]
1514#[napi]
1515pub struct NativeSimulatorHandle {
1516    program: celox::LaidOutProgram,
1517    four_state: bool,
1518    layout_json: String,
1519    events_json: String,
1520    hierarchy_json: String,
1521    warnings_json: String,
1522    stable_size: u32,
1523    total_size: u32,
1524}
1525
1526#[cfg(target_arch = "wasm32")]
1527impl NativeSimulatorHandle {
1528    /// Build an N-API/WASI handle directly from an in-memory frontend
1529    /// artifact.
1530    ///
1531    /// Frontend bindings can accept their own artifact type, lower it with
1532    /// `celox-frontend-sdk`, and call this Rust-only adapter API without a JSON
1533    /// serialization boundary.
1534    pub fn from_frontend(
1535        artifact: celox::FrontendArtifact,
1536        options: Option<NapiOptions>,
1537    ) -> Result<Self> {
1538        let opts = parse_options_common(&options)?;
1539        let trace_opts = celox::TraceOptions::default();
1540        let (program, warnings) = celox::compile_frontend_to_sir(
1541            &artifact,
1542            &opts.false_loops,
1543            &opts.true_loops,
1544            opts.four_state,
1545            &trace_opts,
1546            None,
1547            &opts.optimize_options,
1548        )
1549        .map_err(|error| Error::from_reason(error.to_string()))?;
1550
1551        let laid_out = program.into_laid_out(opts.four_state);
1552        let layout = laid_out.layout();
1553        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1554        let events_json = Self::build_events_json(&laid_out);
1555        let warnings_json = format_warnings_json(&warnings);
1556        let stable_size = layout.total_size as u32;
1557        let total_size = layout.merged_total_size as u32;
1558
1559        Ok(Self {
1560            program: laid_out,
1561            four_state: opts.four_state,
1562            layout_json,
1563            events_json,
1564            hierarchy_json: "{}".to_string(),
1565            warnings_json,
1566            stable_size,
1567            total_size,
1568        })
1569    }
1570}
1571
1572#[cfg(target_arch = "wasm32")]
1573#[napi]
1574impl NativeSimulatorHandle {
1575    /// Compile Veryl source code and produce a WASM-oriented handle.
1576    ///
1577    /// Unlike the native (JIT) variant, this handle does NOT execute
1578    /// simulation directly. Instead it exposes `combWasmBytes()` and
1579    /// `eventWasmBytes(name)` for the TS runtime to instantiate in the
1580    /// browser via WebAssembly.
1581    #[napi(constructor)]
1582    pub fn new(
1583        sources: Vec<NapiSourceFile>,
1584        top: String,
1585        options: Option<NapiOptions>,
1586    ) -> Result<Self> {
1587        let opts = parse_options_common(&options)?;
1588        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1589            .into_iter()
1590            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1591            .collect();
1592        append_extra_source(&mut src_pairs, &opts.extra_source);
1593
1594        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1595            .iter()
1596            .map(|(s, p)| (s.as_str(), p.as_path()))
1597            .collect();
1598
1599        let trace_opts = celox::TraceOptions::default();
1600        let (program, warnings) = celox::compile_to_sir(
1601            &source_refs,
1602            &top,
1603            &opts
1604                .false_loops
1605                .iter()
1606                .map(|(f, t)| (f.clone(), t.clone()))
1607                .collect::<Vec<_>>(),
1608            &opts
1609                .true_loops
1610                .iter()
1611                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
1612                .collect::<Vec<_>>(),
1613            opts.four_state,
1614            &trace_opts,
1615            None,
1616            None,
1617            opts.clock_type,
1618            opts.reset_type,
1619            &opts.parameters,
1620            &opts.optimize_options,
1621        )
1622        .map_err(|e| Error::from_reason(format!("{}", e)))?;
1623
1624        let laid_out = program.into_laid_out(opts.four_state);
1625        let layout = laid_out.layout();
1626
1627        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1628        let events_json = Self::build_events_json(&laid_out);
1629        let hierarchy_json = "{}".to_string(); // Hierarchy not available on wasm32
1630        let warnings_json = format_warnings_json(&warnings);
1631
1632        let stable_size = layout.total_size as u32;
1633        let total_size = layout.merged_total_size as u32;
1634        Ok(Self {
1635            program: laid_out,
1636            four_state: opts.four_state,
1637            layout_json,
1638            events_json,
1639            hierarchy_json,
1640            warnings_json,
1641            stable_size,
1642            total_size,
1643        })
1644    }
1645
1646    /// Create a WASM-oriented handle from a versioned external-frontend artifact.
1647    #[napi(factory)]
1648    pub fn from_frontend_artifact(
1649        artifact_json: String,
1650        options: Option<NapiOptions>,
1651    ) -> Result<Self> {
1652        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1653            .map_err(|error| Error::from_reason(error.to_string()))?;
1654        Self::from_frontend(artifact, options)
1655    }
1656
1657    /// Create a new simulator from a Veryl project directory.
1658    #[napi(factory)]
1659    pub fn from_project(
1660        project_path: String,
1661        top: String,
1662        options: Option<NapiOptions>,
1663    ) -> Result<Self> {
1664        let opts = parse_options_common(&options)?;
1665        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1666        append_extra_source(&mut sources, &opts.extra_source);
1667
1668        let source_refs: Vec<(&str, &std::path::Path)> = sources
1669            .iter()
1670            .map(|(s, p)| (s.as_str(), p.as_path()))
1671            .collect();
1672
1673        let trace_opts = celox::TraceOptions::default();
1674        let (program, warnings) = celox::compile_to_sir(
1675            &source_refs,
1676            &top,
1677            &opts
1678                .false_loops
1679                .iter()
1680                .map(|(f, t)| (f.clone(), t.clone()))
1681                .collect::<Vec<_>>(),
1682            &opts
1683                .true_loops
1684                .iter()
1685                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
1686                .collect::<Vec<_>>(),
1687            opts.four_state,
1688            &trace_opts,
1689            None,
1690            Some(metadata),
1691            opts.clock_type,
1692            opts.reset_type,
1693            &opts.parameters,
1694            &opts.optimize_options,
1695        )
1696        .map_err(|e| Error::from_reason(format!("{}", e)))?;
1697
1698        let laid_out = program.into_laid_out(opts.four_state);
1699        let layout = laid_out.layout();
1700
1701        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1702        let events_json = Self::build_events_json(&laid_out);
1703        let hierarchy_json = "{}".to_string();
1704        let warnings_json = format_warnings_json(&warnings);
1705
1706        let stable_size = layout.total_size as u32;
1707        let total_size = layout.merged_total_size as u32;
1708        Ok(Self {
1709            program: laid_out,
1710            four_state: opts.four_state,
1711            layout_json,
1712            events_json,
1713            hierarchy_json,
1714            warnings_json,
1715            stable_size,
1716            total_size,
1717        })
1718    }
1719
1720    /// Returns the signal layout as a JSON string.
1721    #[napi(getter)]
1722    pub fn layout_json(&self) -> String {
1723        self.layout_json.clone()
1724    }
1725
1726    /// Returns the event map as a JSON string.
1727    #[napi(getter)]
1728    pub fn events_json(&self) -> String {
1729        self.events_json.clone()
1730    }
1731
1732    /// Byte ranges whose value and mask planes must start as unknown (X).
1733    #[napi(getter)]
1734    pub fn four_state_init_regions_json(&self) -> String {
1735        Self::build_four_state_init_regions_json(
1736            &self.program,
1737            self.program.layout(),
1738            self.four_state,
1739        )
1740    }
1741
1742    /// Return a complete initialized memory image for the TypeScript WASM bridge.
1743    #[napi]
1744    pub fn initial_memory_bytes(&self) -> Vec<u8> {
1745        Self::build_initial_memory_bytes(&self.program, self.program.layout(), self.four_state)
1746    }
1747
1748    /// Returns the instance hierarchy as a JSON string.
1749    #[napi(getter)]
1750    pub fn hierarchy_json(&self) -> String {
1751        self.hierarchy_json.clone()
1752    }
1753
1754    /// Returns compilation warnings as a JSON array of strings.
1755    #[napi(getter)]
1756    pub fn warnings_json(&self) -> String {
1757        self.warnings_json.clone()
1758    }
1759
1760    /// Returns the stable region size in bytes.
1761    #[napi(getter)]
1762    pub fn stable_size(&self) -> u32 {
1763        self.stable_size
1764    }
1765
1766    /// Returns the total memory size in bytes.
1767    #[napi(getter)]
1768    pub fn total_size(&self) -> u32 {
1769        self.total_size
1770    }
1771
1772    /// Returns the WASM module bytes for eval_comb (combinational logic evaluation).
1773    #[napi]
1774    pub fn comb_wasm_bytes(&self) -> Vec<u8> {
1775        let wasm = celox::wasm_codegen::compile_units(
1776            &self.program.sir.eval_comb,
1777            self.program.layout(),
1778            self.four_state,
1779            false,
1780        );
1781        wasm.bytes
1782    }
1783
1784    /// Returns the WASM module bytes for a specific clock/reset event.
1785    ///
1786    /// `event_name` should match a clock or reset port name (e.g. "clk", "rst").
1787    #[napi]
1788    pub fn event_wasm_bytes(&self, event_name: String) -> Result<Vec<u8>> {
1789        for (addr, units) in &self.program.sir.eval_apply_ffs {
1790            let event_path = self.program.get_path(addr);
1791            if event_path == event_name {
1792                let wasm = celox::wasm_codegen::compile_units(
1793                    units,
1794                    self.program.layout(),
1795                    self.four_state,
1796                    false,
1797                );
1798                return Ok(wasm.bytes);
1799            }
1800        }
1801
1802        Err(Error::from_reason(format!(
1803            "Event '{}' not found. Available events: {}",
1804            event_name,
1805            self.program
1806                .sir
1807                .eval_apply_ffs
1808                .keys()
1809                .map(|addr| self.program.get_path(addr))
1810                .collect::<Vec<_>>()
1811                .join(", ")
1812        )))
1813    }
1814
1815    /// No-op on wasm32 (no native resources to release).
1816    #[napi]
1817    pub fn dispose(&mut self) {}
1818}
1819
1820#[cfg(target_arch = "wasm32")]
1821impl NativeSimulatorHandle {
1822    fn write_memory_bit(memory: &mut [u8], byte: usize, bit: usize, value: bool) {
1823        let mask = 1u8 << bit;
1824        if value {
1825            memory[byte] |= mask;
1826        } else {
1827            memory[byte] &= !mask;
1828        }
1829    }
1830
1831    fn byte_bit(bytes: &[u8], bit: usize) -> bool {
1832        bytes
1833            .get(bit / 8)
1834            .is_some_and(|byte| byte & (1u8 << (bit % 8)) != 0)
1835    }
1836
1837    fn build_initial_memory_bytes(
1838        program: &celox::LaidOutProgram,
1839        layout: &celox::MemoryLayout,
1840        four_state: bool,
1841    ) -> Vec<u8> {
1842        let mut memory = vec![0u8; layout.merged_total_size];
1843
1844        if four_state {
1845            for (address, &offset) in &layout.offsets {
1846                if layout.is_4states.get(address).copied().unwrap_or(false) {
1847                    let plane_size = layout.plane_size(address);
1848                    memory[offset..offset + plane_size * 2].fill(0xff);
1849                }
1850            }
1851            for (address, &relative_offset) in &layout.working_offsets {
1852                if layout.is_4states.get(address).copied().unwrap_or(false) {
1853                    let offset = layout.working_base_offset + relative_offset;
1854                    let plane_size = layout.plane_size(address);
1855                    memory[offset..offset + plane_size * 2].fill(0xff);
1856                }
1857            }
1858        }
1859
1860        for initial in &program.design.initial_state {
1861            let Some(&offset) = layout.offsets.get(&initial.address) else {
1862                continue;
1863            };
1864            let width = layout.widths[&initial.address];
1865            let plane_size = layout.plane_size(&initial.address);
1866            let write_mask = four_state
1867                && layout
1868                    .is_4states
1869                    .get(&initial.address)
1870                    .copied()
1871                    .unwrap_or(false);
1872
1873            match &initial.data {
1874                celox_design::InitialStateData::Packed {
1875                    value,
1876                    mask,
1877                    written_mask,
1878                } => {
1879                    let value = value.to_bytes_le();
1880                    let mask = mask.to_bytes_le();
1881                    let written_mask = written_mask.to_bytes_le();
1882                    for bit in 0..width {
1883                        if !Self::byte_bit(&written_mask, bit) {
1884                            continue;
1885                        }
1886                        let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
1887                        let is_unknown = Self::byte_bit(&mask, bit);
1888                        Self::write_memory_bit(
1889                            &mut memory,
1890                            offset + byte,
1891                            intra,
1892                            Self::byte_bit(&value, bit) && (write_mask || !is_unknown),
1893                        );
1894                        if write_mask {
1895                            Self::write_memory_bit(
1896                                &mut memory,
1897                                offset + plane_size + byte,
1898                                intra,
1899                                is_unknown,
1900                            );
1901                        }
1902                    }
1903                }
1904                celox_design::InitialStateData::Writes(runs) => {
1905                    for run in runs {
1906                        for relative_bit in 0..run.bit_width {
1907                            let bit = run.bit_offset + relative_bit;
1908                            if bit >= width {
1909                                break;
1910                            }
1911                            let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
1912                            Self::write_memory_bit(
1913                                &mut memory,
1914                                offset + byte,
1915                                intra,
1916                                Self::byte_bit(&run.value_bytes, relative_bit),
1917                            );
1918                            if write_mask {
1919                                Self::write_memory_bit(
1920                                    &mut memory,
1921                                    offset + plane_size + byte,
1922                                    intra,
1923                                    Self::byte_bit(&run.mask_bytes, relative_bit),
1924                                );
1925                            }
1926                        }
1927                    }
1928                }
1929            }
1930        }
1931
1932        memory
1933    }
1934
1935    fn build_four_state_init_regions_json(
1936        program: &celox::LaidOutProgram,
1937        layout: &celox::MemoryLayout,
1938        four_state: bool,
1939    ) -> String {
1940        if !four_state {
1941            return "[]".to_string();
1942        }
1943
1944        let mut regions = Vec::new();
1945        for (addr, &offset) in &layout.offsets {
1946            if program
1947                .design
1948                .state_objects
1949                .get(addr)
1950                .is_some_and(|metadata| metadata.is_4state)
1951            {
1952                regions.push((offset, layout.plane_size(addr)));
1953            }
1954        }
1955        for (addr, &relative_offset) in &layout.working_offsets {
1956            if program
1957                .design
1958                .state_objects
1959                .get(addr)
1960                .is_some_and(|metadata| metadata.is_4state)
1961            {
1962                regions.push((
1963                    layout.working_base_offset + relative_offset,
1964                    layout.plane_size(addr),
1965                ));
1966            }
1967        }
1968        regions.sort_unstable();
1969
1970        serde_json::to_string(&regions).unwrap_or_else(|_| "[]".to_string())
1971    }
1972
1973    /// Build signal layout JSON from finalized SIR and MemoryLayout.
1974    /// Mirrors the layout format from celox-wasm.
1975    fn build_layout_json(
1976        program: &celox::LaidOutProgram,
1977        layout: &celox::MemoryLayout,
1978        four_state: bool,
1979    ) -> String {
1980        use std::collections::BTreeMap;
1981
1982        let mut layout_map: BTreeMap<String, serde_json::Value> = BTreeMap::new();
1983
1984        for addr in program.design.state_objects.keys() {
1985            let Some(variable) = program.design.variable(addr) else {
1986                continue;
1987            };
1988            let Some(instance) = program.design.instance(addr.instance_id) else {
1989                continue;
1990            };
1991            if !instance.resolves_path_to(&variable.path, *addr) {
1992                continue;
1993            }
1994            let metadata = &program.design.state_objects[addr];
1995            let Some(&offset) = layout.offsets.get(addr) else {
1996                continue;
1997            };
1998            let name = program.get_path(addr);
1999            let total_width = layout.widths.get(addr).copied().unwrap_or(0);
2000            let (width, array_dims) = if metadata.array_dims.is_empty() {
2001                (total_width, None)
2002            } else {
2003                let element_count = metadata.array_dims.iter().product::<usize>();
2004                (total_width / element_count, Some(&metadata.array_dims))
2005            };
2006            let byte_size = celox::get_byte_size(width);
2007            let mut entry = serde_json::json!({
2008                "offset": offset,
2009                "width": width,
2010                "byte_size": byte_size,
2011                "is_4state": four_state && metadata.is_4state,
2012                "direction": layout::direction_str(variable.var_kind),
2013                "type_kind": layout::type_kind_str(metadata.type_kind),
2014            });
2015            if let Some(array_dims) = array_dims {
2016                entry["array_dims"] = serde_json::json!(array_dims);
2017            }
2018            layout_map.insert(name, entry);
2019        }
2020
2021        serde_json::to_string(&layout_map).unwrap_or_else(|_| "{}".to_string())
2022    }
2023
2024    /// Build events JSON from finalized SIR.
2025    fn build_events_json(program: &celox::LaidOutProgram) -> String {
2026        use std::collections::BTreeMap;
2027
2028        let mut events: BTreeMap<String, usize> = BTreeMap::new();
2029
2030        for (next_id, addr) in program.sir.eval_apply_ffs.keys().enumerate() {
2031            let name = program.get_path(addr);
2032            events.insert(name, next_id);
2033        }
2034
2035        serde_json::to_string(&events).unwrap_or_else(|_| "{}".to_string())
2036    }
2037}
2038
2039/// Convert an `AnalyzerError` to structured `JsonDiagnostic`s.
2040/// `all_sources` maps path → source content for offset → line:col conversion.
2041fn analyzer_error_to_diagnostics(
2042    err: &veryl_analyzer::AnalyzerError,
2043    all_sources: &[(String, String)],
2044    is_error: bool,
2045) -> Vec<celox_ts_gen::JsonDiagnostic> {
2046    use miette::Diagnostic as _;
2047
2048    let severity = if is_error {
2049        celox_ts_gen::DiagnosticSeverity::Error
2050    } else {
2051        celox_ts_gen::DiagnosticSeverity::Warning
2052    };
2053
2054    let message = format!("{err}");
2055    let help = err.help().map(|h| h.to_string());
2056    let url = err.url().map(|u| u.to_string());
2057
2058    let labels: Vec<_> = err.labels().map(|l| l.collect()).unwrap_or_default();
2059
2060    if labels.is_empty() {
2061        return vec![celox_ts_gen::JsonDiagnostic {
2062            severity,
2063            message,
2064            file: String::new(),
2065            line: 1,
2066            column: 1,
2067            end_line: None,
2068            end_column: None,
2069            help,
2070            url,
2071        }];
2072    }
2073
2074    labels
2075        .into_iter()
2076        .map(|label| {
2077            let offset = label.offset();
2078            let len = label.len();
2079
2080            // Find which source file this offset belongs to
2081            // AnalyzerError uses global offsets across concatenated sources,
2082            // but typically the error_location is within a single file.
2083            // Try each source to find line:col.
2084            let mut file = String::new();
2085            let mut line = 1usize;
2086            let mut col = 1usize;
2087            let mut end_line = None;
2088            let mut end_col = None;
2089
2090            for (path, content) in all_sources {
2091                // miette offsets are per-file when Parser::parse is given the source
2092                let mut cur_line = 1;
2093                let mut cur_col = 1;
2094                let mut found = false;
2095
2096                for (i, ch) in content.char_indices() {
2097                    if i == offset {
2098                        file = path.clone();
2099                        line = cur_line;
2100                        col = cur_col;
2101                        found = true;
2102                    }
2103                    if found && i == offset + len {
2104                        end_line = Some(cur_line);
2105                        end_col = Some(cur_col);
2106                        break;
2107                    }
2108                    if ch == '\n' {
2109                        cur_line += 1;
2110                        cur_col = 1;
2111                    } else {
2112                        cur_col += 1;
2113                    }
2114                }
2115
2116                if found {
2117                    if end_line.is_none() {
2118                        end_line = Some(cur_line);
2119                        end_col = Some(cur_col);
2120                    }
2121                    break;
2122                }
2123            }
2124
2125            celox_ts_gen::JsonDiagnostic {
2126                severity: severity.clone(),
2127                message: label.label().unwrap_or(&message).to_string(),
2128                file,
2129                line,
2130                column: col,
2131                end_line,
2132                end_column: end_col,
2133                help: help.clone(),
2134                url: url.clone(),
2135            }
2136        })
2137        .collect()
2138}
2139
2140/// Format analyzer errors with accumulated warnings for gen_ts error messages.
2141fn format_errors_with_warnings(
2142    pass_label: &str,
2143    errors: &[&veryl_analyzer::AnalyzerError],
2144    warnings: &[veryl_analyzer::AnalyzerError],
2145) -> String {
2146    let error_msgs: Vec<String> = errors
2147        .iter()
2148        .map(|e| celox::render_diagnostic(*e))
2149        .collect();
2150    let mut msg = format!("Errors in {pass_label}: {}", error_msgs.join("; "));
2151    if !warnings.is_empty() {
2152        let warning_msgs: Vec<String> = warnings
2153            .iter()
2154            .map(|w| celox::render_diagnostic(w))
2155            .collect();
2156        msg.push_str("\n\n--- warnings ---\n\n");
2157        msg.push_str(&warning_msgs.join("\n"));
2158    }
2159    msg
2160}
2161
2162/// Clear the process-global JIT compilation cache.
2163///
2164/// Call this when source files have changed and cached compiled code may be stale.
2165#[cfg(not(target_arch = "wasm32"))]
2166#[napi]
2167pub fn clear_jit_cache() {
2168    let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
2169    cache.clear();
2170}
2171
2172/// Stub for wasm32: no JIT cache to clear.
2173#[cfg(target_arch = "wasm32")]
2174#[napi]
2175pub fn clear_jit_cache() {}
2176
2177// ---------------------------------------------------------------------------
2178//  Native testbench execution
2179// ---------------------------------------------------------------------------
2180
2181/// Result of a single `$assert` evaluation in a native testbench.
2182#[cfg(not(target_arch = "wasm32"))]
2183#[napi(object)]
2184pub struct NapiAssertionResult {
2185    pub passed: bool,
2186    pub message: Option<String>,
2187    pub file: Option<String>,
2188    pub line: Option<u32>,
2189    pub column: Option<u32>,
2190}
2191
2192/// Detailed result of running a native testbench.
2193#[cfg(not(target_arch = "wasm32"))]
2194#[napi(object)]
2195pub struct NapiTestResult {
2196    pub passed: bool,
2197    pub assertions: Vec<NapiAssertionResult>,
2198    pub error: Option<String>,
2199}
2200
2201#[cfg(not(target_arch = "wasm32"))]
2202fn convert_test_result(r: celox::TestResultDetailed) -> NapiTestResult {
2203    NapiTestResult {
2204        passed: r.passed,
2205        error: r.error,
2206        assertions: r
2207            .assertions
2208            .into_iter()
2209            .map(|a| {
2210                let (file, line, column) = match a.location {
2211                    Some(loc) => (Some(loc.file), Some(loc.line), Some(loc.column)),
2212                    None => (None, None, None),
2213                };
2214                NapiAssertionResult {
2215                    passed: a.passed,
2216                    message: a.message,
2217                    file,
2218                    line,
2219                    column,
2220                }
2221            })
2222            .collect(),
2223    }
2224}
2225
2226#[cfg(not(target_arch = "wasm32"))]
2227#[napi(object)]
2228pub struct NapiInjectedValue {
2229    pub name: Option<String>,
2230    pub bits: Option<BigInt>,
2231    pub mask_xz: Option<BigInt>,
2232    pub width: Option<u32>,
2233    pub string_value: Option<String>,
2234}
2235
2236#[cfg(not(target_arch = "wasm32"))]
2237#[napi(object)]
2238pub struct NapiInjectedCall {
2239    pub instance: String,
2240    pub phase: String,
2241    pub method: Option<String>,
2242    pub inputs: Vec<NapiInjectedValue>,
2243    pub params: Vec<NapiInjectedValue>,
2244    pub ports: Vec<NapiInjectedPort>,
2245    pub args: Vec<NapiInjectedValue>,
2246    pub cycle: BigInt,
2247    pub time: BigInt,
2248    pub seed: BigInt,
2249    pub fired_clock: Option<String>,
2250    pub four_state: bool,
2251}
2252
2253#[cfg(not(target_arch = "wasm32"))]
2254#[napi(object)]
2255pub struct NapiInjectedPort {
2256    pub name: String,
2257    pub direction: String,
2258    pub role: Option<String>,
2259    pub width: u32,
2260}
2261
2262#[cfg(not(target_arch = "wasm32"))]
2263#[napi(object)]
2264pub struct NapiInjectedResult {
2265    pub outputs: Option<Vec<NapiInjectedValue>>,
2266    pub return_value: Option<NapiInjectedValue>,
2267    pub failures: Option<Vec<String>>,
2268    pub logs: Option<Vec<String>>,
2269    pub finish: Option<bool>,
2270}
2271
2272#[cfg(not(target_arch = "wasm32"))]
2273#[napi(object, object_to_js = false)]
2274pub struct NapiInjectedComponent {
2275    pub name: String,
2276    pub manifest: String,
2277    pub handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2278}
2279
2280#[cfg(not(target_arch = "wasm32"))]
2281struct NapiInjectedHandler {
2282    env: Env,
2283    handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2284}
2285
2286// Injected callbacks are only accepted by the synchronous runTest APIs and
2287// are invoked on the JS thread which supplied this Env. The core trait is
2288// Send + Sync because compiled component hooks may otherwise be movable.
2289#[cfg(not(target_arch = "wasm32"))]
2290unsafe impl Send for NapiInjectedHandler {}
2291#[cfg(not(target_arch = "wasm32"))]
2292unsafe impl Sync for NapiInjectedHandler {}
2293
2294#[cfg(not(target_arch = "wasm32"))]
2295fn napi_bigint(value: u64) -> BigInt {
2296    BigInt {
2297        sign_bit: false,
2298        words: vec![value],
2299    }
2300}
2301
2302#[cfg(not(target_arch = "wasm32"))]
2303fn to_napi_injected_value(name: Option<String>, value: celox::InjectedValue) -> NapiInjectedValue {
2304    match value {
2305        celox::InjectedValue::Bits {
2306            words,
2307            mask_xz,
2308            width,
2309        } => NapiInjectedValue {
2310            name,
2311            bits: Some(BigInt {
2312                sign_bit: false,
2313                words,
2314            }),
2315            mask_xz: Some(BigInt {
2316                sign_bit: false,
2317                words: mask_xz,
2318            }),
2319            width: Some(width),
2320            string_value: None,
2321        },
2322        celox::InjectedValue::String(value) => NapiInjectedValue {
2323            name,
2324            bits: None,
2325            mask_xz: None,
2326            width: None,
2327            string_value: Some(value),
2328        },
2329        celox::InjectedValue::Unit => NapiInjectedValue {
2330            name,
2331            bits: None,
2332            mask_xz: None,
2333            width: None,
2334            string_value: None,
2335        },
2336    }
2337}
2338
2339#[cfg(not(target_arch = "wasm32"))]
2340fn from_napi_injected_value(
2341    value: NapiInjectedValue,
2342) -> std::result::Result<celox::InjectedValue, String> {
2343    if let Some(bits) = value.bits {
2344        if bits.sign_bit || value.mask_xz.as_ref().is_some_and(|mask| mask.sign_bit) {
2345            return Err("component callback values cannot be negative".into());
2346        }
2347        let width = value
2348            .width
2349            .ok_or_else(|| "component callback bit value has no width".to_string())?;
2350        return Ok(celox::InjectedValue::Bits {
2351            words: bits.words,
2352            mask_xz: value.mask_xz.map(|mask| mask.words).unwrap_or_default(),
2353            width,
2354        });
2355    }
2356    Ok(match value.string_value {
2357        Some(value) => celox::InjectedValue::String(value),
2358        None => celox::InjectedValue::Unit,
2359    })
2360}
2361
2362#[cfg(not(target_arch = "wasm32"))]
2363impl celox::InjectedComponentHandler for NapiInjectedHandler {
2364    fn call(
2365        &self,
2366        call: celox::InjectedCall,
2367    ) -> std::result::Result<celox::InjectedResult, String> {
2368        let (phase, method, args) = match call.hook {
2369            celox::InjectedHook::Create => ("create", None, Vec::new()),
2370            celox::InjectedHook::Init => ("init", None, Vec::new()),
2371            celox::InjectedHook::Reset => ("reset", None, Vec::new()),
2372            celox::InjectedHook::Clock => ("clock", None, Vec::new()),
2373            celox::InjectedHook::Finish => ("finish", None, Vec::new()),
2374            celox::InjectedHook::Method { name, args } => ("method", Some(name), args),
2375        };
2376        let request = NapiInjectedCall {
2377            instance: call.instance,
2378            phase: phase.into(),
2379            method,
2380            inputs: call
2381                .inputs
2382                .into_iter()
2383                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2384                .collect(),
2385            params: call
2386                .params
2387                .into_iter()
2388                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2389                .collect(),
2390            ports: call
2391                .ports
2392                .into_iter()
2393                .map(|port| NapiInjectedPort {
2394                    name: port.name,
2395                    direction: port.direction,
2396                    role: port.role,
2397                    width: port.width,
2398                })
2399                .collect(),
2400            args: args
2401                .into_iter()
2402                .map(|value| to_napi_injected_value(None, value))
2403                .collect(),
2404            cycle: napi_bigint(call.cycle),
2405            time: napi_bigint(call.time),
2406            seed: napi_bigint(call.seed),
2407            fired_clock: call.fired_clock,
2408            four_state: call.four_state,
2409        };
2410        let result = self
2411            .handler
2412            .borrow_back(&self.env)
2413            .and_then(|handler| handler.call(request))
2414            .map_err(|error| error.to_string())?;
2415        let outputs = result
2416            .outputs
2417            .unwrap_or_default()
2418            .into_iter()
2419            .map(|value| {
2420                let name = value
2421                    .name
2422                    .clone()
2423                    .ok_or_else(|| "component callback output has no name".to_string())?;
2424                Ok(celox::InjectedNamedValue {
2425                    name,
2426                    value: from_napi_injected_value(value)?,
2427                })
2428            })
2429            .collect::<std::result::Result<Vec<_>, String>>()?;
2430        Ok(celox::InjectedResult {
2431            outputs,
2432            return_value: result
2433                .return_value
2434                .map(from_napi_injected_value)
2435                .transpose()?,
2436            failures: result.failures.unwrap_or_default(),
2437            logs: result.logs.unwrap_or_default(),
2438            finish: result.finish.unwrap_or(false),
2439        })
2440    }
2441}
2442
2443#[cfg(not(target_arch = "wasm32"))]
2444fn injected_components(
2445    env: Env,
2446    definitions: Option<Vec<NapiInjectedComponent>>,
2447) -> Result<celox::InjectedComponents> {
2448    let mut components = celox::InjectedComponents::new();
2449    for definition in definitions.unwrap_or_default() {
2450        components
2451            .insert(
2452                definition.name,
2453                &definition.manifest,
2454                Arc::new(NapiInjectedHandler {
2455                    env,
2456                    handler: definition.handler,
2457                }),
2458            )
2459            .map_err(Error::from_reason)?;
2460    }
2461    Ok(components)
2462}
2463
2464/// Run a native testbench from Veryl source code.
2465///
2466/// Compiles the given sources and runs the `#[test]` module specified by `top`,
2467/// returning assertion results observed before that test finishes or stops on a
2468/// fatal failure.
2469#[cfg(not(target_arch = "wasm32"))]
2470#[napi]
2471pub fn run_test(
2472    env: Env,
2473    sources: Vec<NapiSourceFile>,
2474    top: String,
2475    options: Option<NapiOptions>,
2476    components: Option<Vec<NapiInjectedComponent>>,
2477) -> Result<NapiTestResult> {
2478    let opts = parse_options(&options)?;
2479    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
2480        .into_iter()
2481        .map(|s| (s.content, std::path::PathBuf::from(s.path)))
2482        .collect();
2483    append_extra_source(&mut src_pairs, &opts.extra_source);
2484
2485    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
2486        .iter()
2487        .map(|(s, p)| (s.as_str(), p.as_path()))
2488        .collect();
2489    let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts)
2490        .with_injected_components(injected_components(env, components)?);
2491    let result = builder
2492        .run_test_detailed()
2493        .map_err(|e| Error::from_reason(format!("{e}")))?;
2494    Ok(convert_test_result(result))
2495}
2496
2497/// Run a Veryl native testbench against an external frontend artifact.
2498#[cfg(not(target_arch = "wasm32"))]
2499#[napi]
2500pub fn run_test_with_frontend_artifact(
2501    env: Env,
2502    artifact_json: String,
2503    sources: Vec<NapiSourceFile>,
2504    top: String,
2505    options: Option<NapiOptions>,
2506    components: Option<Vec<NapiInjectedComponent>>,
2507) -> Result<NapiTestResult> {
2508    let opts = parse_options(&options)?;
2509    let artifact = celox::FrontendArtifact::from_json(&artifact_json)
2510        .map_err(|error| Error::from_reason(error.to_string()))?;
2511    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
2512        .into_iter()
2513        .map(|source| (source.content, std::path::PathBuf::from(source.path)))
2514        .collect();
2515    append_extra_source(&mut src_pairs, &opts.extra_source);
2516    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
2517        .iter()
2518        .map(|(source, path)| (source.as_str(), path.as_path()))
2519        .collect();
2520    let builder = apply_options(
2521        celox::Simulator::from_frontend_with_testbench(artifact, source_refs, &top),
2522        &opts,
2523    )
2524    .with_injected_components(injected_components(env, components)?);
2525    let result = builder
2526        .run_test_detailed()
2527        .map_err(|error| Error::from_reason(error.to_string()))?;
2528    Ok(convert_test_result(result))
2529}
2530
2531/// Run a native testbench from a Veryl project directory.
2532///
2533/// Searches upward from `project_path` for `Veryl.toml`, gathers all
2534/// `.veryl` source files, and runs the `#[test]` module specified by `top`.
2535#[cfg(not(target_arch = "wasm32"))]
2536#[napi]
2537pub fn run_test_from_project(
2538    env: Env,
2539    project_path: String,
2540    top: String,
2541    options: Option<NapiOptions>,
2542    components: Option<Vec<NapiInjectedComponent>>,
2543) -> Result<NapiTestResult> {
2544    let opts = parse_options(&options)?;
2545    let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
2546    append_extra_source(&mut sources, &opts.extra_source);
2547
2548    let source_refs: Vec<(&str, &std::path::Path)> = sources
2549        .iter()
2550        .map(|(s, p)| (s.as_str(), p.as_path()))
2551        .collect();
2552    let builder = apply_options(
2553        celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
2554        &opts,
2555    )
2556    .with_injected_components(injected_components(env, components)?);
2557    let result = builder
2558        .run_test_detailed()
2559        .map_err(|e| Error::from_reason(format!("{e}")))?;
2560    Ok(convert_test_result(result))
2561}
2562
2563// ---------------------------------------------------------------------------
2564//  TypeScript type generation
2565// ---------------------------------------------------------------------------
2566
2567#[napi(object)]
2568pub struct NapiInjectedManifest {
2569    pub name: String,
2570    pub manifest: String,
2571}
2572
2573fn inject_analyzer_components(definitions: Option<Vec<NapiInjectedManifest>>) -> Result<()> {
2574    let definitions = definitions.unwrap_or_default();
2575    let names: Vec<_> = definitions
2576        .iter()
2577        .map(|definition| definition.name.as_str())
2578        .collect();
2579    veryl_analyzer::tb_component::insert_external_components(&names);
2580    for definition in definitions {
2581        let manifest =
2582            veryl_metadata::ComponentManifest::parse(&definition.manifest).ok_or_else(|| {
2583                Error::from_reason(format!(
2584                    "manifest of injected component `{}` cannot be parsed",
2585                    definition.name
2586                ))
2587            })?;
2588        veryl_analyzer::component_manifest_table::insert(
2589            veryl_parser::resource_table::insert_str(&definition.name),
2590            manifest,
2591        );
2592    }
2593    Ok(())
2594}
2595
2596/// Generate TypeScript type information as JSON for a Veryl project.
2597///
2598/// Equivalent to running `celox-gen-ts --json` from the given project directory.
2599#[napi]
2600pub fn gen_ts(
2601    project_path: String,
2602    components: Option<Vec<NapiInjectedManifest>>,
2603) -> Result<String> {
2604    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
2605
2606    let toml_path = Metadata::search_from(&project_path)
2607        .map_err(|e| Error::from_reason(format!("Could not find Veryl.toml: {e}")))?;
2608    let mut metadata = Metadata::load(&toml_path)
2609        .map_err(|e| Error::from_reason(format!("Failed to load Veryl.toml: {e}")))?;
2610
2611    let base_path = toml_path
2612        .parent()
2613        .unwrap_or(&toml_path)
2614        .to_string_lossy()
2615        .to_string();
2616
2617    let mut paths = metadata
2618        .paths::<std::path::PathBuf>(&[], true, true)
2619        .map_err(|e| Error::from_reason(format!("Failed to gather sources: {e}")))?;
2620    paths.retain(|path| !path.example);
2621
2622    // Append test-only sources declared in celox.toml
2623    let project_root = toml_path.parent().unwrap_or(&toml_path).to_path_buf();
2624    let celox_cfg = load_celox_config(&project_root)?;
2625    let prj_name = metadata.project.name.clone();
2626    for dir in &celox_cfg.test.sources {
2627        let dir_path = project_root.join(dir);
2628        if !dir_path.exists() {
2629            continue;
2630        }
2631        for src in walkdir(&dir_path)? {
2632            paths.push(PathSet {
2633                prj: prj_name.clone(),
2634                src: src.clone(),
2635                dst: src.with_extension("sv"),
2636                map: src.with_extension("map"),
2637                example: false,
2638            });
2639        }
2640    }
2641
2642    if let Some(exclude_set) = build_exclude_set(&celox_cfg)? {
2643        paths.retain(|p| !is_excluded(&p.src, &project_root, &exclude_set));
2644    }
2645
2646    if paths.is_empty() {
2647        return Err(Error::from_reason("No Veryl source files found"));
2648    }
2649
2650    // Parse and analyze pass 1
2651    symbol_table::clear();
2652    attribute_table::clear();
2653
2654    let analyzer = Analyzer::new(&metadata);
2655    inject_analyzer_components(components)?;
2656    let mut parsers = Vec::new();
2657    let mut all_warnings = Vec::new();
2658
2659    for path in &paths {
2660        let input = std::fs::read_to_string(&path.src)
2661            .map_err(|e| Error::from_reason(format!("{}: {e}", path.src.display())))?;
2662        let parser = Parser::parse(&input, &path.src)
2663            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
2664
2665        let results = analyzer.analyze_pass1(&path.prj, &parser.veryl);
2666        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2667        if !real_errors.is_empty() {
2668            return Err(Error::from_reason(format_errors_with_warnings(
2669                "analysis pass 1",
2670                &real_errors,
2671                &all_warnings,
2672            )));
2673        }
2674        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2675
2676        parsers.push((path.clone(), parser));
2677    }
2678
2679    let results = Analyzer::analyze_post_pass1();
2680    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2681    if !real_errors.is_empty() {
2682        return Err(Error::from_reason(format_errors_with_warnings(
2683            "post-pass 1 analysis",
2684            &real_errors,
2685            &all_warnings,
2686        )));
2687    }
2688    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2689
2690    // Pass 2: per-file IR → generate
2691
2692    // Compute all source file relative paths for embedding in generated JS.
2693    let base_normalized = base_path.replace('\\', "/");
2694    let all_source_files: Vec<String> = parsers
2695        .iter()
2696        .map(|(path, _)| {
2697            let src_normalized = path
2698                .src
2699                .to_string_lossy()
2700                .replace(r"\\?\", "")
2701                .replace('\\', "/");
2702            src_normalized
2703                .strip_prefix(&base_normalized)
2704                .unwrap_or(&src_normalized)
2705                .trim_start_matches('/')
2706                .to_string()
2707        })
2708        .collect();
2709    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
2710
2711    let mut all_modules = Vec::new();
2712    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
2713    let mut post_pass_ir = Ir::default();
2714
2715    for (i, (_path, parser)) in parsers.iter().enumerate() {
2716        let mut analyzer_context = Context::default();
2717        let mut ir = Ir::default();
2718        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
2719        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2720        if !real_errors.is_empty() {
2721            return Err(Error::from_reason(format_errors_with_warnings(
2722                "analysis pass 2",
2723                &real_errors,
2724                &all_warnings,
2725            )));
2726        }
2727        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2728
2729        let modules = generate_all(&ir, &source_file_refs);
2730        post_pass_ir.append(&mut ir);
2731        let source_file = all_source_files[i].clone();
2732
2733        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
2734        if !module_names.is_empty() {
2735            file_modules.insert(source_file.clone(), module_names);
2736        }
2737
2738        for m in modules {
2739            all_modules.push(JsonModuleEntry {
2740                module_name: m.module_name,
2741                source_file: source_file.clone(),
2742                dts_content: m.dts_content,
2743                md_content: m.md_content,
2744                ports: m.ports,
2745                events: m.events,
2746                instances: m.instances,
2747                is_test: m.is_test,
2748            });
2749        }
2750    }
2751
2752    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
2753    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2754    if !real_errors.is_empty() {
2755        return Err(Error::from_reason(format_errors_with_warnings(
2756            "post-pass 2 analysis",
2757            &real_errors,
2758            &all_warnings,
2759        )));
2760    }
2761    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2762
2763    // Sort for deterministic output
2764    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
2765
2766    let warning_msgs: Vec<String> = all_warnings
2767        .iter()
2768        .map(|w| celox::render_diagnostic(w))
2769        .collect();
2770
2771    let all_sources: Vec<(String, String)> = parsers
2772        .iter()
2773        .map(|(p, _)| {
2774            let path_str = p.src.to_string_lossy().to_string();
2775            let content = std::fs::read_to_string(&p.src).unwrap_or_default();
2776            (path_str, content)
2777        })
2778        .collect();
2779
2780    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
2781        .iter()
2782        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
2783        .collect();
2784
2785    let output = JsonOutput {
2786        project_path: base_path,
2787        modules: all_modules,
2788        file_modules,
2789        warnings: warning_msgs,
2790        diagnostics,
2791    };
2792
2793    serde_json::to_string(&output)
2794        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
2795}
2796
2797/// Generate TypeScript type information from in-memory Veryl sources.
2798///
2799/// Like `gen_ts()` but does not require a Veryl.toml or filesystem access.
2800/// Works on both native and wasm32 targets.
2801#[napi]
2802pub fn gen_ts_from_source(
2803    sources: Vec<NapiSourceFile>,
2804    components: Option<Vec<NapiInjectedManifest>>,
2805) -> Result<String> {
2806    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
2807
2808    if sources.is_empty() {
2809        return Err(Error::from_reason("No source files provided"));
2810    }
2811
2812    let metadata = Metadata::create_default("playground")
2813        .map_err(|e| Error::from_reason(format!("Failed to create default metadata: {e}")))?;
2814
2815    // Parse and analyze pass 1
2816    symbol_table::clear();
2817    attribute_table::clear();
2818
2819    let analyzer = Analyzer::new(&metadata);
2820    inject_analyzer_components(components)?;
2821    let mut parsers = Vec::new();
2822    let mut all_warnings = Vec::new();
2823
2824    for src in &sources {
2825        let path = std::path::PathBuf::from(&src.path);
2826        let parser = Parser::parse(&src.content, &path)
2827            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
2828
2829        let prj = "playground".to_string();
2830        let results = analyzer.analyze_pass1(&prj, &parser.veryl);
2831        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2832        if !real_errors.is_empty() {
2833            return Err(Error::from_reason(format_errors_with_warnings(
2834                "analysis pass 1",
2835                &real_errors,
2836                &all_warnings,
2837            )));
2838        }
2839        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2840
2841        parsers.push((prj, path, parser));
2842    }
2843
2844    let results = Analyzer::analyze_post_pass1();
2845    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2846    if !real_errors.is_empty() {
2847        return Err(Error::from_reason(format_errors_with_warnings(
2848            "post-pass 1 analysis",
2849            &real_errors,
2850            &all_warnings,
2851        )));
2852    }
2853    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2854
2855    // Pass 2: per-file IR → generate
2856    let all_source_files: Vec<String> = parsers
2857        .iter()
2858        .map(|(_, p, _)| p.to_string_lossy().replace('\\', "/"))
2859        .collect();
2860    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
2861
2862    let mut all_modules = Vec::new();
2863    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
2864    let mut post_pass_ir = Ir::default();
2865
2866    for (i, (_prj, _path, parser)) in parsers.iter().enumerate() {
2867        let mut analyzer_context = Context::default();
2868        let mut ir = Ir::default();
2869        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
2870        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2871        if !real_errors.is_empty() {
2872            return Err(Error::from_reason(format_errors_with_warnings(
2873                "analysis pass 2",
2874                &real_errors,
2875                &all_warnings,
2876            )));
2877        }
2878        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2879
2880        let modules = generate_all(&ir, &source_file_refs);
2881        post_pass_ir.append(&mut ir);
2882        let source_file = all_source_files[i].clone();
2883
2884        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
2885        if !module_names.is_empty() {
2886            file_modules.insert(source_file.clone(), module_names);
2887        }
2888
2889        for m in modules {
2890            all_modules.push(JsonModuleEntry {
2891                module_name: m.module_name,
2892                source_file: source_file.clone(),
2893                dts_content: m.dts_content,
2894                md_content: m.md_content,
2895                ports: m.ports,
2896                events: m.events,
2897                instances: m.instances,
2898                is_test: m.is_test,
2899            });
2900        }
2901    }
2902
2903    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
2904    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2905    if !real_errors.is_empty() {
2906        return Err(Error::from_reason(format_errors_with_warnings(
2907            "post-pass 2 analysis",
2908            &real_errors,
2909            &all_warnings,
2910        )));
2911    }
2912    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2913
2914    // Sort for deterministic output
2915    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
2916
2917    let warning_msgs: Vec<String> = all_warnings
2918        .iter()
2919        .map(|w| celox::render_diagnostic(w))
2920        .collect();
2921
2922    let all_sources: Vec<(String, String)> = sources
2923        .iter()
2924        .map(|s| (s.path.clone(), s.content.clone()))
2925        .collect();
2926
2927    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
2928        .iter()
2929        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
2930        .collect();
2931
2932    let output = JsonOutput {
2933        project_path: String::new(),
2934        modules: all_modules,
2935        file_modules,
2936        warnings: warning_msgs,
2937        diagnostics,
2938    };
2939
2940    serde_json::to_string(&output)
2941        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
2942}
2943
2944#[cfg(all(test, not(target_arch = "wasm32")))]
2945mod tests {
2946    use super::*;
2947
2948    fn default_opts() -> ParsedOptions {
2949        ParsedOptions {
2950            common: ParsedOptionsCommon {
2951                four_state: false,
2952                optimize_options: celox::OptimizeOptions::all(),
2953                vcd: None,
2954                false_loops: vec![],
2955                true_loops: vec![],
2956                clock_type: None,
2957                reset_type: None,
2958                extra_source: None,
2959                parameters: vec![],
2960            },
2961            cranelift_options: celox::CraneliftOptions::default(),
2962            dead_store_policy: celox::DeadStorePolicy::Off,
2963        }
2964    }
2965
2966    fn make_sources(pairs: &[(&str, &str)]) -> Vec<(String, std::path::PathBuf)> {
2967        pairs
2968            .iter()
2969            .map(|(content, path)| (content.to_string(), std::path::PathBuf::from(path)))
2970            .collect()
2971    }
2972
2973    #[test]
2974    fn simulator_handle_derives_four_state_metadata_from_layout() {
2975        let source = "module Top (a: input logic<1>) {}";
2976        let path = std::path::Path::new("top.veryl");
2977
2978        for four_state in [false, true] {
2979            let simulator = celox::Simulator::from_sources(vec![(source, path)], "Top")
2980                .four_state(four_state)
2981                .build()
2982                .unwrap();
2983            let handle = NativeSimulatorHandle::from_simulator(simulator, None).unwrap();
2984            let layout: serde_json::Value = serde_json::from_str(&handle.layout_json()).unwrap();
2985
2986            assert_eq!(layout["a"]["is_4state"].as_bool(), Some(four_state));
2987        }
2988    }
2989
2990    #[test]
2991    fn normal_project_loading_excludes_example_sources() {
2992        let project = tempfile::tempdir().unwrap();
2993        std::fs::create_dir(project.path().join("src")).unwrap();
2994        std::fs::create_dir(project.path().join("examples")).unwrap();
2995        std::fs::write(
2996            project.path().join("Veryl.toml"),
2997            "[project]\nname = \"example_filter\"\nversion = \"0.1.0\"\n",
2998        )
2999        .unwrap();
3000        std::fs::write(project.path().join("src/top.veryl"), "module Top () {}\n").unwrap();
3001        std::fs::write(
3002            project.path().join("examples/demo.veryl"),
3003            "module Demo () {}\n",
3004        )
3005        .unwrap();
3006
3007        let (sources, _, _) = load_project_sources(project.path().to_str().unwrap()).unwrap();
3008        let source_names = sources
3009            .iter()
3010            .map(|(_, path)| path.file_name().unwrap().to_str().unwrap())
3011            .collect::<Vec<_>>();
3012        assert_eq!(source_names, ["top.veryl"]);
3013    }
3014
3015    #[test]
3016    fn same_inputs_produce_same_key() {
3017        let src = make_sources(&[("module Top {}", "a.veryl")]);
3018        let opts = default_opts();
3019        let k1 = build_cache_key(&src, "Top", &opts, None);
3020        let k2 = build_cache_key(&src, "Top", &opts, None);
3021        assert_eq!(k1, k2);
3022    }
3023
3024    #[test]
3025    fn high_index_sir_pass_changes_cache_key_without_bit_packing() {
3026        let src = make_sources(&[("module Top {}", "a.veryl")]);
3027        let enabled = default_opts();
3028        let mut disabled = default_opts();
3029        disabled.common.optimize_options = disabled
3030            .common
3031            .optimize_options
3032            .clone()
3033            .disable(celox::SirPass::IdentityStoreBypass);
3034
3035        assert_ne!(
3036            build_cache_key(&src, "Top", &enabled, None),
3037            build_cache_key(&src, "Top", &disabled, None)
3038        );
3039    }
3040
3041    #[test]
3042    fn native_memory_width_changes_cache_key() {
3043        let src = make_sources(&[("module Top {}", "a.veryl")]);
3044        let mut narrow = default_opts();
3045        narrow.common.optimize_options = narrow
3046            .common
3047            .optimize_options
3048            .clone()
3049            .with_max_native_memory_width(64);
3050        let mut wide = default_opts();
3051        wide.common.optimize_options = wide
3052            .common
3053            .optimize_options
3054            .clone()
3055            .with_max_native_memory_width(128);
3056
3057        assert_ne!(
3058            build_cache_key(&src, "Top", &narrow, None),
3059            build_cache_key(&src, "Top", &wide, None)
3060        );
3061    }
3062
3063    #[test]
3064    fn different_source_content_different_key() {
3065        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3066        let s2 = make_sources(&[("module B {}", "a.veryl")]);
3067        let opts = default_opts();
3068        assert_ne!(
3069            build_cache_key(&s1, "Top", &opts, None),
3070            build_cache_key(&s2, "Top", &opts, None),
3071        );
3072    }
3073
3074    #[test]
3075    fn different_source_path_different_key() {
3076        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3077        let s2 = make_sources(&[("module A {}", "b.veryl")]);
3078        let opts = default_opts();
3079        assert_ne!(
3080            build_cache_key(&s1, "Top", &opts, None),
3081            build_cache_key(&s2, "Top", &opts, None),
3082        );
3083    }
3084
3085    #[test]
3086    fn different_top_different_key() {
3087        let src = make_sources(&[("module Top {}", "a.veryl")]);
3088        let opts = default_opts();
3089        assert_ne!(
3090            build_cache_key(&src, "Top", &opts, None),
3091            build_cache_key(&src, "Other", &opts, None),
3092        );
3093    }
3094
3095    #[test]
3096    fn four_state_differs() {
3097        let src = make_sources(&[("module Top {}", "a.veryl")]);
3098        let mut o1 = default_opts();
3099        let mut o2 = default_opts();
3100        o1.common.four_state = false;
3101        o2.common.four_state = true;
3102        assert_ne!(
3103            build_cache_key(&src, "Top", &o1, None),
3104            build_cache_key(&src, "Top", &o2, None),
3105        );
3106    }
3107
3108    #[test]
3109    fn optimize_options_differs() {
3110        let src = make_sources(&[("module Top {}", "a.veryl")]);
3111        let mut o1 = default_opts();
3112        let mut o2 = default_opts();
3113        o1.common.optimize_options = celox::OptimizeOptions::all();
3114        o2.common.optimize_options = celox::OptimizeOptions::none();
3115        assert_ne!(
3116            build_cache_key(&src, "Top", &o1, None),
3117            build_cache_key(&src, "Top", &o2, None),
3118        );
3119    }
3120
3121    #[test]
3122    fn cranelift_opt_level_differs() {
3123        let src = make_sources(&[("module Top {}", "a.veryl")]);
3124        let mut o1 = default_opts();
3125        let mut o2 = default_opts();
3126        o1.cranelift_options.opt_level = celox::CraneliftOptLevel::Speed;
3127        o2.cranelift_options.opt_level = celox::CraneliftOptLevel::None;
3128        assert_ne!(
3129            build_cache_key(&src, "Top", &o1, None),
3130            build_cache_key(&src, "Top", &o2, None),
3131        );
3132    }
3133
3134    #[test]
3135    fn regalloc_algorithm_differs() {
3136        let src = make_sources(&[("module Top {}", "a.veryl")]);
3137        let mut o1 = default_opts();
3138        let mut o2 = default_opts();
3139        o1.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::Backtracking;
3140        o2.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::SinglePass;
3141        assert_ne!(
3142            build_cache_key(&src, "Top", &o1, None),
3143            build_cache_key(&src, "Top", &o2, None),
3144        );
3145    }
3146
3147    #[test]
3148    fn dead_store_policy_differs() {
3149        let src = make_sources(&[("module Top {}", "a.veryl")]);
3150        let mut o1 = default_opts();
3151        let mut o2 = default_opts();
3152        o1.dead_store_policy = celox::DeadStorePolicy::Off;
3153        o2.dead_store_policy = celox::DeadStorePolicy::PreserveTopPorts;
3154        assert_ne!(
3155            build_cache_key(&src, "Top", &o1, None),
3156            build_cache_key(&src, "Top", &o2, None),
3157        );
3158    }
3159
3160    #[test]
3161    fn clock_type_differs() {
3162        let src = make_sources(&[("module Top {}", "a.veryl")]);
3163        let mut o1 = default_opts();
3164        let mut o2 = default_opts();
3165        o1.common.clock_type = None;
3166        o2.common.clock_type = Some(celox::ClockType::NegEdge);
3167        assert_ne!(
3168            build_cache_key(&src, "Top", &o1, None),
3169            build_cache_key(&src, "Top", &o2, None),
3170        );
3171    }
3172
3173    #[test]
3174    fn reset_type_differs() {
3175        let src = make_sources(&[("module Top {}", "a.veryl")]);
3176        let mut o1 = default_opts();
3177        let mut o2 = default_opts();
3178        o1.common.reset_type = None;
3179        o2.common.reset_type = Some(celox::ResetType::SyncHigh);
3180        assert_ne!(
3181            build_cache_key(&src, "Top", &o1, None),
3182            build_cache_key(&src, "Top", &o2, None),
3183        );
3184    }
3185
3186    #[test]
3187    fn parameters_differ() {
3188        let src = make_sources(&[("module Top {}", "a.veryl")]);
3189        let mut o1 = default_opts();
3190        let mut o2 = default_opts();
3191        o1.common.parameters = vec![("WIDTH".into(), 8)];
3192        o2.common.parameters = vec![("WIDTH".into(), 16)];
3193        assert_ne!(
3194            build_cache_key(&src, "Top", &o1, None),
3195            build_cache_key(&src, "Top", &o2, None),
3196        );
3197    }
3198
3199    #[test]
3200    fn source_order_independent() {
3201        let s1 = make_sources(&[("aaa", "a.veryl"), ("bbb", "b.veryl")]);
3202        let s2 = make_sources(&[("bbb", "b.veryl"), ("aaa", "a.veryl")]);
3203        let opts = default_opts();
3204        assert_eq!(
3205            build_cache_key(&s1, "Top", &opts, None),
3206            build_cache_key(&s2, "Top", &opts, None),
3207        );
3208    }
3209
3210    #[test]
3211    fn non_compilation_options_ignored() {
3212        let src = make_sources(&[("module Top {}", "a.veryl")]);
3213        let mut o1 = default_opts();
3214        let mut o2 = default_opts();
3215        // VCD path doesn't affect compilation
3216        o1.common.vcd = None;
3217        o2.common.vcd = Some("/tmp/dump.vcd".into());
3218        assert_eq!(
3219            build_cache_key(&src, "Top", &o1, None),
3220            build_cache_key(&src, "Top", &o2, None),
3221        );
3222    }
3223
3224    #[test]
3225    fn false_loops_differ() {
3226        let src = make_sources(&[("module Top {}", "a.veryl")]);
3227        let mut o1 = default_opts();
3228        let mut o2 = default_opts();
3229        o1.common.false_loops = vec![];
3230        o2.common.false_loops = vec![((vec![], vec!["a".into()]), (vec![], vec!["b".into()]))];
3231        assert_ne!(
3232            build_cache_key(&src, "Top", &o1, None),
3233            build_cache_key(&src, "Top", &o2, None),
3234        );
3235    }
3236
3237    #[test]
3238    fn true_loops_differ() {
3239        let src = make_sources(&[("module Top {}", "a.veryl")]);
3240        let mut o1 = default_opts();
3241        let mut o2 = default_opts();
3242        o1.common.true_loops = vec![];
3243        o2.common.true_loops = vec![((vec![], vec!["x".into()]), (vec![], vec!["y".into()]), 4)];
3244        assert_ne!(
3245            build_cache_key(&src, "Top", &o1, None),
3246            build_cache_key(&src, "Top", &o2, None),
3247        );
3248    }
3249
3250    #[test]
3251    fn metadata_clock_reset_differs() {
3252        let src = make_sources(&[("module Top {}", "a.veryl")]);
3253        let opts = default_opts();
3254
3255        let mut m1 = Metadata::create_default("prj").unwrap();
3256        let mut m2 = Metadata::create_default("prj").unwrap();
3257        m1.build.clock_type = celox::ClockType::PosEdge;
3258        m2.build.clock_type = celox::ClockType::NegEdge;
3259        assert_ne!(
3260            build_cache_key(&src, "Top", &opts, Some(&m1)),
3261            build_cache_key(&src, "Top", &opts, Some(&m2)),
3262        );
3263
3264        let mut m3 = Metadata::create_default("prj").unwrap();
3265        let mut m4 = Metadata::create_default("prj").unwrap();
3266        m3.build.reset_type = celox::ResetType::AsyncLow;
3267        m4.build.reset_type = celox::ResetType::SyncHigh;
3268        assert_ne!(
3269            build_cache_key(&src, "Top", &opts, Some(&m3)),
3270            build_cache_key(&src, "Top", &opts, Some(&m4)),
3271        );
3272    }
3273
3274    #[test]
3275    fn no_metadata_vs_metadata_differs() {
3276        let src = make_sources(&[("module Top {}", "a.veryl")]);
3277        let opts = default_opts();
3278        let m = Metadata::create_default("prj").unwrap();
3279        // No metadata vs with metadata should differ (metadata adds clock/reset info)
3280        assert_ne!(
3281            build_cache_key(&src, "Top", &opts, None),
3282            build_cache_key(&src, "Top", &opts, Some(&m)),
3283        );
3284    }
3285}