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/// The backend driving a [`NativeSimulatorHandle`].
826///
827/// Either the default compiled backend (as before) or the tiered backend,
828/// which starts on the interpreter and promotes to generated code in the
829/// background. Dispatch goes through the shared [`SimBackend`] surface the
830/// handle actually uses.
831#[cfg(not(target_arch = "wasm32"))]
832enum HandleBackend {
833    Default(celox::DefaultBackend),
834    Tiered(Box<celox::TieredBackend>),
835}
836
837#[cfg(not(target_arch = "wasm32"))]
838impl HandleBackend {
839    fn eval_comb(&mut self) -> std::result::Result<(), celox::RuntimeErrorCode> {
840        match self {
841            Self::Default(backend) => backend.eval_comb(),
842            Self::Tiered(backend) => backend.eval_comb(),
843        }
844    }
845
846    fn eval_apply_ff_at(
847        &mut self,
848        event_id: usize,
849    ) -> std::result::Result<(), celox::RuntimeErrorCode> {
850        match self {
851            Self::Default(backend) => {
852                let event = backend.id_to_event_slice()[event_id];
853                backend.eval_apply_ff_at(event)
854            }
855            Self::Tiered(backend) => {
856                let event = backend.id_to_event_slice()[event_id];
857                backend.eval_apply_ff_at(event)
858            }
859        }
860    }
861
862    fn memory_as_ptr(&self) -> (*const u8, usize) {
863        match self {
864            Self::Default(backend) => backend.memory_as_ptr(),
865            Self::Tiered(backend) => backend.memory_as_ptr(),
866        }
867    }
868
869    fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
870        match self {
871            Self::Default(backend) => backend.memory_as_mut_ptr(),
872            Self::Tiered(backend) => backend.memory_as_mut_ptr(),
873        }
874    }
875
876    fn stable_region_size(&self) -> usize {
877        match self {
878            Self::Default(backend) => backend.stable_region_size(),
879            Self::Tiered(backend) => backend.stable_region_size(),
880        }
881    }
882}
883
884/// Low-level handle wrapping the default backend and optional VCD writer.
885///
886/// JS holds this as an opaque class; all operations go through methods.
887#[cfg(not(target_arch = "wasm32"))]
888#[napi]
889pub struct NativeSimulatorHandle {
890    backend: Option<HandleBackend>,
891    runtime_errors: HashMap<i64, (String, Vec<String>)>,
892    vcd_writer: Option<celox::VcdWriter>,
893    layout_json: String,
894    events_json: String,
895    hierarchy_json: String,
896    warnings_json: String,
897    stable_size: u32,
898    total_size: u32,
899}
900
901#[cfg(not(target_arch = "wasm32"))]
902impl NativeSimulatorHandle {
903    /// Wrap a simulator built by an external frontend in the standard Celox
904    /// N-API handle.
905    ///
906    /// This is a Rust-only adapter API. A frontend binding can accept its own
907    /// artifact type in a `#[napi]` function, lower it with
908    /// `celox-frontend-sdk`, build a [`celox::Simulator`], and pass the value
909    /// here without serializing an artifact to JSON. Signal metadata is always
910    /// derived from the simulator's actual memory layout.
911    pub fn from_simulator(simulator: celox::Simulator, vcd_path: Option<&str>) -> Result<Self> {
912        Self::build_and_cache(simulator, vcd_path, None)
913    }
914
915    /// Build an N-API handle directly from an in-memory frontend artifact.
916    ///
917    /// Frontend bindings normally expose an artifact-specific function such
918    /// as `from_my_artifact` and use this as their final adapter step.
919    pub fn from_frontend(
920        artifact: celox::FrontendArtifact,
921        options: Option<NapiOptions>,
922    ) -> Result<Self> {
923        let opts = parse_options(&options)?;
924        let builder = apply_options(celox::Simulator::from_frontend(artifact), &opts);
925        let simulator = builder
926            .build()
927            .map_err(|error| Error::from_reason(error.to_string()))?;
928        Self::from_simulator(simulator, opts.vcd.as_deref())
929    }
930}
931
932#[cfg(not(target_arch = "wasm32"))]
933#[napi]
934impl NativeSimulatorHandle {
935    /// Build a full simulator, extract metadata, cache the compiled code,
936    /// and return the handle with the default backend (and optional VcdWriter).
937    fn build_and_cache(
938        sim: celox::Simulator,
939        vcd_path: Option<&str>,
940        cache_key: Option<CacheKey>,
941    ) -> Result<Self> {
942        let four_state = sim.layout().four_state;
943        let warnings_json = format_warnings_json(sim.warnings());
944        let signals = sim.named_signals();
945        let events = sim.named_events();
946        let hierarchy = sim.named_hierarchy();
947        let (_, total_size) = sim.memory_as_ptr();
948        let stable_size = sim.stable_region_size();
949        let vcd_descs = sim.build_vcd_descs(four_state);
950        let runtime_errors = runtime_errors_by_name(sim.program());
951
952        let layout_map = build_signal_layout(&signals, four_state);
953        let event_map = build_event_map(&events);
954        let hierarchy_node = build_hierarchy_node(&hierarchy, four_state);
955
956        let layout_json = serde_json::to_string(&layout_map)
957            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
958        let events_json = serde_json::to_string(&event_map)
959            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
960        let hierarchy_json = serde_json::to_string(&hierarchy_node)
961            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
962
963        // Cache the compiled code + metadata for future instances
964        if let Some(key) = cache_key {
965            let cached = Arc::new(CachedBuild {
966                shared_code: sim.shared_code(),
967                runtime_errors: runtime_errors.clone(),
968                layout_json: layout_json.clone(),
969                events_json: events_json.clone(),
970                hierarchy_json: hierarchy_json.clone(),
971                warnings_json: warnings_json.clone(),
972                stable_size: stable_size as u32,
973                total_size: total_size as u32,
974                vcd_descs: vcd_descs.clone(),
975            });
976            let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
977            cache.insert(key, cached);
978        }
979
980        // Create VcdWriter if requested
981        let vcd_writer = if let Some(path) = vcd_path {
982            Some(
983                celox::VcdWriter::new(path, &vcd_descs)
984                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
985            )
986        } else {
987            None
988        };
989
990        // Extract the backend from Simulator (drops runtime metadata which is no longer needed)
991        let backend = HandleBackend::Default(sim.into_backend());
992
993        Ok(Self {
994            backend: Some(backend),
995            runtime_errors,
996            vcd_writer,
997            layout_json,
998            events_json,
999            hierarchy_json,
1000            warnings_json,
1001            stable_size: stable_size as u32,
1002            total_size: total_size as u32,
1003        })
1004    }
1005
1006    /// Extract metadata from a tiered simulator and wrap its backend.
1007    ///
1008    /// Mirrors [`Self::build_and_cache`] minus the shared-code cache: the
1009    /// tiered backend owns its interpreter state and compiles in the
1010    /// background, so there is no compiled artifact to reuse yet.
1011    fn build_and_cache_tiered(mut sim: celox::Simulator<celox::TieredBackend>) -> Result<Self> {
1012        // The builder creates the VCD writer itself when recording was
1013        // requested, selecting a VCD-compatible packed layout in the process;
1014        // reuse that writer instead of rebuilding descriptors here.
1015        let vcd_writer = sim.take_vcd_writer();
1016        let four_state = sim.layout().four_state;
1017        let warnings_json = format_warnings_json(sim.warnings());
1018        let signals = sim.named_signals();
1019        let events = sim.named_events();
1020        let hierarchy = sim.named_hierarchy();
1021        let (_, total_size) = sim.memory_as_ptr();
1022        let stable_size = sim.stable_region_size();
1023        let runtime_errors = runtime_errors_by_name(sim.program());
1024
1025        let layout_map = build_signal_layout(&signals, four_state);
1026        let event_map = build_event_map(&events);
1027        let hierarchy_node = build_hierarchy_node(&hierarchy, four_state);
1028
1029        let layout_json = serde_json::to_string(&layout_map)
1030            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1031        let events_json = serde_json::to_string(&event_map)
1032            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1033        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1034            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1035
1036        // Extract the backend from Simulator (drops runtime metadata which is
1037        // no longer needed).
1038        let backend = HandleBackend::Tiered(Box::new(sim.into_backend()));
1039
1040        Ok(Self {
1041            backend: Some(backend),
1042            runtime_errors,
1043            vcd_writer,
1044            layout_json,
1045            events_json,
1046            hierarchy_json,
1047            warnings_json,
1048            stable_size: stable_size as u32,
1049            total_size: total_size as u32,
1050        })
1051    }
1052
1053    /// Create a handle from a cached build (shared compiled code + fresh memory).
1054    fn from_cached(cached: &CachedBuild, vcd_path: Option<&str>) -> Result<Self> {
1055        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1056        let backend = HandleBackend::Default(celox::NativeBackend::from_shared(Arc::clone(
1057            &cached.shared_code,
1058        )));
1059        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1060        let backend = HandleBackend::Default(celox::JitBackend::from_shared(Arc::clone(
1061            &cached.shared_code,
1062        )));
1063        let vcd_writer = if let Some(path) = vcd_path {
1064            Some(
1065                celox::VcdWriter::new(path, &cached.vcd_descs)
1066                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
1067            )
1068        } else {
1069            None
1070        };
1071        Ok(Self {
1072            backend: Some(backend),
1073            runtime_errors: cached.runtime_errors.clone(),
1074            vcd_writer,
1075            layout_json: cached.layout_json.clone(),
1076            events_json: cached.events_json.clone(),
1077            hierarchy_json: cached.hierarchy_json.clone(),
1078            warnings_json: cached.warnings_json.clone(),
1079            stable_size: cached.stable_size,
1080            total_size: cached.total_size,
1081        })
1082    }
1083
1084    /// Create a new simulator from Veryl source code.
1085    #[napi(constructor)]
1086    pub fn new(
1087        sources: Vec<NapiSourceFile>,
1088        top: String,
1089        options: Option<NapiOptions>,
1090    ) -> Result<Self> {
1091        let opts = parse_options(&options)?;
1092        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1093            .into_iter()
1094            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1095            .collect();
1096        append_extra_source(&mut src_pairs, &opts.extra_source);
1097
1098        let cache_key = build_cache_key(&src_pairs, &top, &opts, None);
1099
1100        {
1101            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1102            if let Some(cached) = cache.get(&cache_key) {
1103                return Self::from_cached(cached, opts.vcd.as_deref());
1104            }
1105        }
1106
1107        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1108            .iter()
1109            .map(|(s, p)| (s.as_str(), p.as_path()))
1110            .collect();
1111        let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts);
1112        let sim = builder
1113            .build()
1114            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1115
1116        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1117    }
1118
1119    /// Create a new tiered simulator from Veryl source code.
1120    ///
1121    /// Execution starts on the interpreter immediately while the host's
1122    /// default compiled tier (native where available) is prepared on a
1123    /// background thread. The first safe point after compilation completes
1124    /// adopts the compiled code; observe progress through
1125    /// [`Self::tier_compiled`](Self::tier_compiled). Tiered builds bypass the
1126    /// JIT cache because the interpreter already removes startup latency.
1127    #[napi(factory)]
1128    pub fn new_tiered(
1129        sources: Vec<NapiSourceFile>,
1130        top: String,
1131        options: Option<NapiOptions>,
1132    ) -> Result<Self> {
1133        let opts = parse_options(&options)?;
1134        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1135            .into_iter()
1136            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1137            .collect();
1138        append_extra_source(&mut src_pairs, &opts.extra_source);
1139
1140        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1141            .iter()
1142            .map(|(s, p)| (s.as_str(), p.as_path()))
1143            .collect();
1144        let mut builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts);
1145        // Forward VCD before building: tiered layout selection must know that
1146        // recording was requested so it picks the packed layout the VCD
1147        // descriptors require (see `build_and_cache_tiered`).
1148        if let Some(path) = opts.vcd.as_deref() {
1149            builder = builder.vcd(path);
1150        }
1151        let sim = builder
1152            .build_tiered()
1153            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1154
1155        Self::build_and_cache_tiered(sim)
1156    }
1157
1158    /// Create a new tiered simulator from a Veryl project directory.
1159    ///
1160    /// Tiered counterpart of [`Self::from_project`]: searches upward from
1161    /// `project_path` for `Veryl.toml`, starts on the interpreter
1162    /// immediately, and promotes to the host's default compiled tier in the
1163    /// background. Bypasses the JIT cache for the same reason as
1164    /// [`Self::new_tiered`].
1165    #[napi(factory)]
1166    pub fn new_tiered_from_project(
1167        project_path: String,
1168        top: String,
1169        options: Option<NapiOptions>,
1170    ) -> Result<Self> {
1171        let opts = parse_options(&options)?;
1172        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1173        append_extra_source(&mut sources, &opts.extra_source);
1174
1175        let source_refs: Vec<(&str, &std::path::Path)> = sources
1176            .iter()
1177            .map(|(s, p)| (s.as_str(), p.as_path()))
1178            .collect();
1179
1180        let mut builder = apply_options(
1181            celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
1182            &opts,
1183        );
1184        // Forward VCD before building: tiered layout selection must know that
1185        // recording was requested so it picks the packed layout the VCD
1186        // descriptors require (see `build_and_cache_tiered`).
1187        if let Some(path) = opts.vcd.as_deref() {
1188            builder = builder.vcd(path);
1189        }
1190        let sim = builder
1191            .build_tiered()
1192            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1193
1194        Self::build_and_cache_tiered(sim)
1195    }
1196
1197    /// Create a simulator from a versioned external-frontend artifact.
1198    #[napi(factory)]
1199    pub fn from_frontend_artifact(
1200        artifact_json: String,
1201        options: Option<NapiOptions>,
1202    ) -> Result<Self> {
1203        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1204            .map_err(|error| Error::from_reason(error.to_string()))?;
1205        Self::from_frontend(artifact, options)
1206    }
1207
1208    /// Create a new simulator from a Veryl project directory.
1209    ///
1210    /// Searches upward from `project_path` for `Veryl.toml`, gathers all
1211    /// `.veryl` source files, and builds the simulator using the project's
1212    /// clock/reset settings.
1213    #[napi(factory)]
1214    pub fn from_project(
1215        project_path: String,
1216        top: String,
1217        options: Option<NapiOptions>,
1218    ) -> Result<Self> {
1219        let opts = parse_options(&options)?;
1220        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1221        append_extra_source(&mut sources, &opts.extra_source);
1222
1223        let cache_key = build_cache_key(&sources, &top, &opts, Some(&metadata));
1224
1225        {
1226            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1227            if let Some(cached) = cache.get(&cache_key) {
1228                return Self::from_cached(cached, opts.vcd.as_deref());
1229            }
1230        }
1231
1232        let source_refs: Vec<(&str, &std::path::Path)> = sources
1233            .iter()
1234            .map(|(s, p)| (s.as_str(), p.as_path()))
1235            .collect();
1236
1237        let builder = apply_options(
1238            celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
1239            &opts,
1240        );
1241        let sim = builder
1242            .build()
1243            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1244
1245        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1246    }
1247
1248    /// Returns the signal layout as a JSON string.
1249    #[napi(getter)]
1250    pub fn layout_json(&self) -> String {
1251        self.layout_json.clone()
1252    }
1253
1254    /// Returns the event map as a JSON string.
1255    #[napi(getter)]
1256    pub fn events_json(&self) -> String {
1257        self.events_json.clone()
1258    }
1259
1260    /// Returns the instance hierarchy as a JSON string.
1261    #[napi(getter)]
1262    pub fn hierarchy_json(&self) -> String {
1263        self.hierarchy_json.clone()
1264    }
1265
1266    /// Returns compilation warnings as a JSON array of strings.
1267    #[napi(getter)]
1268    pub fn warnings_json(&self) -> String {
1269        self.warnings_json.clone()
1270    }
1271
1272    /// Returns the stable region size in bytes.
1273    #[napi(getter)]
1274    pub fn stable_size(&self) -> u32 {
1275        self.stable_size
1276    }
1277
1278    /// Returns the total memory size in bytes.
1279    #[napi(getter)]
1280    pub fn total_size(&self) -> u32 {
1281        self.total_size
1282    }
1283
1284    /// Whether this handle drives the tiered backend.
1285    #[napi(getter)]
1286    pub fn is_tiered(&self) -> bool {
1287        matches!(self.backend, Some(HandleBackend::Tiered(_)))
1288    }
1289
1290    /// Whether the tiered backend has adopted its compiled tier.
1291    ///
1292    /// `None` when the handle is not tiered; `false` while background
1293    /// compilation is still running or after it failed (the interpreter then
1294    /// remains the permanent tier).
1295    #[napi(getter)]
1296    pub fn tier_compiled(&self) -> Option<bool> {
1297        match &self.backend {
1298            Some(HandleBackend::Tiered(backend)) => Some(backend.is_compiled()),
1299            _ => None,
1300        }
1301    }
1302
1303    /// Trigger a clock/event by its numeric ID.
1304    #[napi]
1305    pub fn tick(&mut self, event_id: u32) -> Result<()> {
1306        let runtime_errors = self.runtime_errors.clone();
1307        let b = self
1308            .backend
1309            .as_mut()
1310            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1311        b.eval_comb()
1312            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1313        b.eval_apply_ff_at(event_id as usize)
1314            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1315        b.eval_comb()
1316            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1317    }
1318
1319    /// Trigger a clock/event N times in a single NAPI call.
1320    #[napi]
1321    pub fn tick_n(&mut self, event_id: u32, count: u32) -> Result<()> {
1322        let runtime_errors = self.runtime_errors.clone();
1323        let b = self
1324            .backend
1325            .as_mut()
1326            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1327        for _ in 0..count {
1328            b.eval_comb()
1329                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1330            b.eval_apply_ff_at(event_id as usize)
1331                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1332            b.eval_comb()
1333                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1334        }
1335        Ok(())
1336    }
1337
1338    /// Evaluate combinational logic.
1339    #[napi]
1340    pub fn eval_comb(&mut self) -> Result<()> {
1341        let runtime_errors = self.runtime_errors.clone();
1342        let b = self
1343            .backend
1344            .as_mut()
1345            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1346        b.eval_comb()
1347            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1348    }
1349
1350    /// Write VCD dump at the given timestamp.
1351    #[napi]
1352    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1353        let b = self
1354            .backend
1355            .as_ref()
1356            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1357        if let Some(ref mut writer) = self.vcd_writer {
1358            let (ptr, size) = b.memory_as_ptr();
1359            let memory = unsafe { std::slice::from_raw_parts(ptr, size) };
1360            writer
1361                .dump(timestamp as u64, memory)
1362                .map_err(|e| Error::from_reason(format!("VCD write error: {}", e)))?;
1363        }
1364        Ok(())
1365    }
1366
1367    /// Return the simulator's stable memory region as a zero-copy `Uint8Array`.
1368    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
1369    #[napi]
1370    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
1371        let b = self
1372            .backend
1373            .as_mut()
1374            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1375        let (ptr, _) = b.memory_as_mut_ptr();
1376        let stable_size = b.stable_region_size();
1377        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, |_, _| {}) })
1378    }
1379
1380    /// Invalidate this handle (no-op on the Rust side; drop happens via GC).
1381    #[napi]
1382    pub fn dispose(&mut self) {
1383        self.backend = None;
1384        self.vcd_writer = None;
1385    }
1386}
1387
1388/// Low-level handle wrapping a `celox::Simulation`.
1389#[cfg(not(target_arch = "wasm32"))]
1390#[napi]
1391pub struct NativeSimulationHandle {
1392    sim: Option<celox::Simulation>,
1393    layout_json: String,
1394    events_json: String,
1395    hierarchy_json: String,
1396    warnings_json: String,
1397    stable_size: u32,
1398    total_size: u32,
1399    /// Default `maxSteps` for `waitUntil` / `waitForCycles`, sourced from
1400    /// `[simulation] max_steps` in `celox.toml`. `None` when not set.
1401    default_max_steps: Option<u32>,
1402}
1403
1404#[cfg(not(target_arch = "wasm32"))]
1405#[napi]
1406impl NativeSimulationHandle {
1407    /// Create a new timed simulation from Veryl source code.
1408    #[napi(constructor)]
1409    pub fn new(
1410        sources: Vec<NapiSourceFile>,
1411        top: String,
1412        options: Option<NapiOptions>,
1413    ) -> Result<Self> {
1414        let opts = parse_options(&options)?;
1415        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1416            .into_iter()
1417            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1418            .collect();
1419        append_extra_source(&mut src_pairs, &opts.extra_source);
1420        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1421            .iter()
1422            .map(|(s, p)| (s.as_str(), p.as_path()))
1423            .collect();
1424        let mut builder = apply_options(celox::Simulation::from_sources(source_refs, &top), &opts);
1425        if let Some(path) = &opts.vcd {
1426            builder = builder.vcd(path);
1427        }
1428        let sim = builder
1429            .build()
1430            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1431
1432        let warnings_json = format_warnings_json(sim.warnings());
1433        let signals = sim.named_signals();
1434        let events = sim.named_events();
1435        let hierarchy = sim.named_hierarchy();
1436        let (_, total_size) = sim.memory_as_ptr();
1437        let stable_size = sim.stable_region_size();
1438
1439        let layout_map = build_signal_layout(&signals, opts.four_state);
1440        let event_map = build_event_map(&events);
1441        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1442
1443        let layout_json = serde_json::to_string(&layout_map)
1444            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1445        let events_json = serde_json::to_string(&event_map)
1446            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1447        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1448            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1449
1450        Ok(Self {
1451            sim: Some(sim),
1452            layout_json,
1453            events_json,
1454            hierarchy_json,
1455            warnings_json,
1456            stable_size: stable_size as u32,
1457            total_size: total_size as u32,
1458            default_max_steps: None,
1459        })
1460    }
1461
1462    /// Create a timed simulation from a versioned external-frontend artifact.
1463    #[napi(factory)]
1464    pub fn from_frontend_artifact(
1465        artifact_json: String,
1466        options: Option<NapiOptions>,
1467    ) -> Result<Self> {
1468        let opts = parse_options(&options)?;
1469        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1470            .map_err(|error| Error::from_reason(error.to_string()))?;
1471        let mut builder = apply_options(celox::Simulation::from_frontend(artifact), &opts);
1472        if let Some(path) = &opts.vcd {
1473            builder = builder.vcd(path);
1474        }
1475        let sim = builder
1476            .build()
1477            .map_err(|error| Error::from_reason(error.to_string()))?;
1478
1479        let warnings_json = format_warnings_json(sim.warnings());
1480        let signals = sim.named_signals();
1481        let events = sim.named_events();
1482        let hierarchy = sim.named_hierarchy();
1483        let (_, total_size) = sim.memory_as_ptr();
1484        let stable_size = sim.stable_region_size();
1485        let layout_map = build_signal_layout(&signals, opts.four_state);
1486        let event_map = build_event_map(&events);
1487        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1488        let layout_json = serde_json::to_string(&layout_map)
1489            .map_err(|error| Error::from_reason(format!("Failed to serialize layout: {error}")))?;
1490        let events_json = serde_json::to_string(&event_map)
1491            .map_err(|error| Error::from_reason(format!("Failed to serialize events: {error}")))?;
1492        let hierarchy_json = serde_json::to_string(&hierarchy_node).map_err(|error| {
1493            Error::from_reason(format!("Failed to serialize hierarchy: {error}"))
1494        })?;
1495
1496        Ok(Self {
1497            sim: Some(sim),
1498            layout_json,
1499            events_json,
1500            hierarchy_json,
1501            warnings_json,
1502            stable_size: stable_size as u32,
1503            total_size: total_size as u32,
1504            default_max_steps: None,
1505        })
1506    }
1507
1508    /// Create a new timed simulation from a Veryl project directory.
1509    #[napi(factory)]
1510    pub fn from_project(
1511        project_path: String,
1512        top: String,
1513        options: Option<NapiOptions>,
1514    ) -> Result<Self> {
1515        let opts = parse_options(&options)?;
1516        let (mut sources, metadata, celox_cfg) = load_project_sources(&project_path)?;
1517        append_extra_source(&mut sources, &opts.extra_source);
1518        let source_refs: Vec<(&str, &std::path::Path)> = sources
1519            .iter()
1520            .map(|(s, p)| (s.as_str(), p.as_path()))
1521            .collect();
1522
1523        let mut builder = apply_options(
1524            celox::Simulation::from_sources(source_refs, &top).with_metadata(metadata),
1525            &opts,
1526        );
1527        if let Some(path) = &opts.vcd {
1528            builder = builder.vcd(path);
1529        }
1530        let sim = builder
1531            .build()
1532            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1533
1534        let warnings_json = format_warnings_json(sim.warnings());
1535        let signals = sim.named_signals();
1536        let events = sim.named_events();
1537        let hierarchy = sim.named_hierarchy();
1538        let (_, total_size) = sim.memory_as_ptr();
1539        let stable_size = sim.stable_region_size();
1540
1541        let layout_map = build_signal_layout(&signals, opts.four_state);
1542        let event_map = build_event_map(&events);
1543        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1544
1545        let layout_json = serde_json::to_string(&layout_map)
1546            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1547        let events_json = serde_json::to_string(&event_map)
1548            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1549        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1550            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1551
1552        Ok(Self {
1553            sim: Some(sim),
1554            layout_json,
1555            events_json,
1556            hierarchy_json,
1557            warnings_json,
1558            stable_size: stable_size as u32,
1559            total_size: total_size as u32,
1560            default_max_steps: celox_cfg.simulation.max_steps,
1561        })
1562    }
1563
1564    /// Returns the signal layout as a JSON string.
1565    #[napi(getter)]
1566    pub fn layout_json(&self) -> String {
1567        self.layout_json.clone()
1568    }
1569
1570    /// Returns the event map as a JSON string.
1571    #[napi(getter)]
1572    pub fn events_json(&self) -> String {
1573        self.events_json.clone()
1574    }
1575
1576    /// Returns the instance hierarchy as a JSON string.
1577    #[napi(getter)]
1578    pub fn hierarchy_json(&self) -> String {
1579        self.hierarchy_json.clone()
1580    }
1581
1582    /// Returns compilation warnings as a JSON array of strings.
1583    #[napi(getter)]
1584    pub fn warnings_json(&self) -> String {
1585        self.warnings_json.clone()
1586    }
1587
1588    /// Returns the stable region size in bytes.
1589    #[napi(getter)]
1590    pub fn stable_size(&self) -> u32 {
1591        self.stable_size
1592    }
1593
1594    /// Returns the total memory size in bytes.
1595    #[napi(getter)]
1596    pub fn total_size(&self) -> u32 {
1597        self.total_size
1598    }
1599
1600    /// Returns the default `maxSteps` from `[simulation] max_steps` in `celox.toml`,
1601    /// or `null` if not configured.
1602    #[napi(getter)]
1603    pub fn default_max_steps(&self) -> Option<u32> {
1604        self.default_max_steps
1605    }
1606
1607    /// Register a clock by event ID.
1608    #[napi]
1609    pub fn add_clock(&mut self, event_id: u32, period: f64, initial_delay: f64) -> Result<()> {
1610        let sim = self
1611            .sim
1612            .as_mut()
1613            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1614        sim.add_clock_by_id(event_id, period as u64, initial_delay as u64);
1615        Ok(())
1616    }
1617
1618    /// Schedule a one-shot event by event ID.
1619    #[napi]
1620    pub fn schedule(&mut self, event_id: u32, time: f64, value: f64) -> Result<()> {
1621        let sim = self
1622            .sim
1623            .as_mut()
1624            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1625        sim.schedule_by_id(event_id, time as u64, value as u64)
1626            .map_err(|e| Error::from_reason(format!("{}", e)))
1627    }
1628
1629    /// Advance simulation until `end_time`.
1630    #[napi]
1631    pub fn run_until(&mut self, end_time: f64) -> Result<()> {
1632        let sim = self
1633            .sim
1634            .as_mut()
1635            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1636        sim.run_until(end_time as u64)
1637            .map_err(|e| Error::from_reason(format!("{}", e)))
1638    }
1639
1640    /// Advance to the next event. Returns the new time, or null if no events.
1641    #[napi]
1642    pub fn step(&mut self) -> Result<Option<f64>> {
1643        let sim = self
1644            .sim
1645            .as_mut()
1646            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1647        sim.step()
1648            .map(|opt| opt.map(|t| t as f64))
1649            .map_err(|e| Error::from_reason(format!("{}", e)))
1650    }
1651
1652    /// Returns the current simulation time.
1653    #[napi]
1654    pub fn time(&self) -> Result<f64> {
1655        let sim = self
1656            .sim
1657            .as_ref()
1658            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1659        Ok(sim.time() as f64)
1660    }
1661
1662    /// Returns the time of the next scheduled event, or null if none.
1663    #[napi]
1664    pub fn next_event_time(&self) -> Result<Option<f64>> {
1665        let sim = self
1666            .sim
1667            .as_ref()
1668            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1669        Ok(sim.next_event_time().map(|t| t as f64))
1670    }
1671
1672    /// Evaluate combinational logic.
1673    #[napi]
1674    pub fn eval_comb(&mut self) -> Result<()> {
1675        let sim = self
1676            .sim
1677            .as_mut()
1678            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1679        sim.eval_comb()
1680            .map_err(|e| Error::from_reason(format!("{}", e)))
1681    }
1682
1683    /// Write VCD dump at the given timestamp.
1684    #[napi]
1685    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1686        let sim = self
1687            .sim
1688            .as_mut()
1689            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1690        sim.dump(timestamp as u64);
1691        Ok(())
1692    }
1693
1694    /// Return the simulation's stable memory region as a zero-copy `Uint8Array`.
1695    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
1696    #[napi]
1697    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
1698        let sim = self
1699            .sim
1700            .as_mut()
1701            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1702        let (ptr, _) = sim.memory_as_mut_ptr();
1703        let stable_size = sim.stable_region_size();
1704        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, |_, _| {}) })
1705    }
1706
1707    /// Invalidate this handle.
1708    #[napi]
1709    pub fn dispose(&mut self) {
1710        self.sim = None;
1711    }
1712}
1713
1714// ---------------------------------------------------------------------------
1715//  WASM32 NativeSimulatorHandle — compiles Veryl to WASM bytecode
1716// ---------------------------------------------------------------------------
1717
1718#[cfg(target_arch = "wasm32")]
1719#[napi]
1720pub struct NativeSimulatorHandle {
1721    program: celox::LaidOutProgram,
1722    four_state: bool,
1723    layout_json: String,
1724    events_json: String,
1725    hierarchy_json: String,
1726    warnings_json: String,
1727    stable_size: u32,
1728    total_size: u32,
1729}
1730
1731#[cfg(target_arch = "wasm32")]
1732impl NativeSimulatorHandle {
1733    /// Build an N-API/WASI handle directly from an in-memory frontend
1734    /// artifact.
1735    ///
1736    /// Frontend bindings can accept their own artifact type, lower it with
1737    /// `celox-frontend-sdk`, and call this Rust-only adapter API without a JSON
1738    /// serialization boundary.
1739    pub fn from_frontend(
1740        artifact: celox::FrontendArtifact,
1741        options: Option<NapiOptions>,
1742    ) -> Result<Self> {
1743        let opts = parse_options_common(&options)?;
1744        let trace_opts = celox::TraceOptions::default();
1745        let (program, warnings) = celox::compile_frontend_to_sir(
1746            &artifact,
1747            &opts.false_loops,
1748            &opts.true_loops,
1749            opts.four_state,
1750            &trace_opts,
1751            None,
1752            &opts.optimize_options,
1753        )
1754        .map_err(|error| Error::from_reason(error.to_string()))?;
1755
1756        let laid_out = program.into_laid_out(opts.four_state);
1757        let layout = laid_out.layout();
1758        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1759        let events_json = Self::build_events_json(&laid_out);
1760        let warnings_json = format_warnings_json(&warnings);
1761        let stable_size = layout.total_size as u32;
1762        let total_size = layout.merged_total_size as u32;
1763
1764        Ok(Self {
1765            program: laid_out,
1766            four_state: opts.four_state,
1767            layout_json,
1768            events_json,
1769            hierarchy_json: "{}".to_string(),
1770            warnings_json,
1771            stable_size,
1772            total_size,
1773        })
1774    }
1775}
1776
1777#[cfg(target_arch = "wasm32")]
1778#[napi]
1779impl NativeSimulatorHandle {
1780    /// Compile Veryl source code and produce a WASM-oriented handle.
1781    ///
1782    /// Unlike the native (JIT) variant, this handle does NOT execute
1783    /// simulation directly. Instead it exposes `combWasmBytes()` and
1784    /// `eventWasmBytes(name)` for the TS runtime to instantiate in the
1785    /// browser via WebAssembly.
1786    #[napi(constructor)]
1787    pub fn new(
1788        sources: Vec<NapiSourceFile>,
1789        top: String,
1790        options: Option<NapiOptions>,
1791    ) -> Result<Self> {
1792        let opts = parse_options_common(&options)?;
1793        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1794            .into_iter()
1795            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1796            .collect();
1797        append_extra_source(&mut src_pairs, &opts.extra_source);
1798
1799        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1800            .iter()
1801            .map(|(s, p)| (s.as_str(), p.as_path()))
1802            .collect();
1803
1804        let trace_opts = celox::TraceOptions::default();
1805        let (program, warnings) = celox::compile_to_sir(
1806            &source_refs,
1807            &top,
1808            &opts
1809                .false_loops
1810                .iter()
1811                .map(|(f, t)| (f.clone(), t.clone()))
1812                .collect::<Vec<_>>(),
1813            &opts
1814                .true_loops
1815                .iter()
1816                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
1817                .collect::<Vec<_>>(),
1818            opts.four_state,
1819            &trace_opts,
1820            None,
1821            None,
1822            opts.clock_type,
1823            opts.reset_type,
1824            &opts.parameters,
1825            &opts.optimize_options,
1826        )
1827        .map_err(|e| Error::from_reason(format!("{}", e)))?;
1828
1829        let laid_out = program.into_laid_out(opts.four_state);
1830        let layout = laid_out.layout();
1831
1832        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1833        let events_json = Self::build_events_json(&laid_out);
1834        let hierarchy_json = "{}".to_string(); // Hierarchy not available on wasm32
1835        let warnings_json = format_warnings_json(&warnings);
1836
1837        let stable_size = layout.total_size as u32;
1838        let total_size = layout.merged_total_size as u32;
1839        Ok(Self {
1840            program: laid_out,
1841            four_state: opts.four_state,
1842            layout_json,
1843            events_json,
1844            hierarchy_json,
1845            warnings_json,
1846            stable_size,
1847            total_size,
1848        })
1849    }
1850
1851    /// Create a WASM-oriented handle from a versioned external-frontend artifact.
1852    #[napi(factory)]
1853    pub fn from_frontend_artifact(
1854        artifact_json: String,
1855        options: Option<NapiOptions>,
1856    ) -> Result<Self> {
1857        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1858            .map_err(|error| Error::from_reason(error.to_string()))?;
1859        Self::from_frontend(artifact, options)
1860    }
1861
1862    /// Create a new simulator from a Veryl project directory.
1863    #[napi(factory)]
1864    pub fn from_project(
1865        project_path: String,
1866        top: String,
1867        options: Option<NapiOptions>,
1868    ) -> Result<Self> {
1869        let opts = parse_options_common(&options)?;
1870        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1871        append_extra_source(&mut sources, &opts.extra_source);
1872
1873        let source_refs: Vec<(&str, &std::path::Path)> = sources
1874            .iter()
1875            .map(|(s, p)| (s.as_str(), p.as_path()))
1876            .collect();
1877
1878        let trace_opts = celox::TraceOptions::default();
1879        let (program, warnings) = celox::compile_to_sir(
1880            &source_refs,
1881            &top,
1882            &opts
1883                .false_loops
1884                .iter()
1885                .map(|(f, t)| (f.clone(), t.clone()))
1886                .collect::<Vec<_>>(),
1887            &opts
1888                .true_loops
1889                .iter()
1890                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
1891                .collect::<Vec<_>>(),
1892            opts.four_state,
1893            &trace_opts,
1894            None,
1895            Some(metadata),
1896            opts.clock_type,
1897            opts.reset_type,
1898            &opts.parameters,
1899            &opts.optimize_options,
1900        )
1901        .map_err(|e| Error::from_reason(format!("{}", e)))?;
1902
1903        let laid_out = program.into_laid_out(opts.four_state);
1904        let layout = laid_out.layout();
1905
1906        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
1907        let events_json = Self::build_events_json(&laid_out);
1908        let hierarchy_json = "{}".to_string();
1909        let warnings_json = format_warnings_json(&warnings);
1910
1911        let stable_size = layout.total_size as u32;
1912        let total_size = layout.merged_total_size as u32;
1913        Ok(Self {
1914            program: laid_out,
1915            four_state: opts.four_state,
1916            layout_json,
1917            events_json,
1918            hierarchy_json,
1919            warnings_json,
1920            stable_size,
1921            total_size,
1922        })
1923    }
1924
1925    /// Returns the signal layout as a JSON string.
1926    #[napi(getter)]
1927    pub fn layout_json(&self) -> String {
1928        self.layout_json.clone()
1929    }
1930
1931    /// Returns the event map as a JSON string.
1932    #[napi(getter)]
1933    pub fn events_json(&self) -> String {
1934        self.events_json.clone()
1935    }
1936
1937    /// Byte ranges whose value and mask planes must start as unknown (X).
1938    #[napi(getter)]
1939    pub fn four_state_init_regions_json(&self) -> String {
1940        Self::build_four_state_init_regions_json(
1941            &self.program,
1942            self.program.layout(),
1943            self.four_state,
1944        )
1945    }
1946
1947    /// Return a complete initialized memory image for the TypeScript WASM bridge.
1948    #[napi]
1949    pub fn initial_memory_bytes(&self) -> Vec<u8> {
1950        Self::build_initial_memory_bytes(&self.program, self.program.layout(), self.four_state)
1951    }
1952
1953    /// Returns the instance hierarchy as a JSON string.
1954    #[napi(getter)]
1955    pub fn hierarchy_json(&self) -> String {
1956        self.hierarchy_json.clone()
1957    }
1958
1959    /// Returns compilation warnings as a JSON array of strings.
1960    #[napi(getter)]
1961    pub fn warnings_json(&self) -> String {
1962        self.warnings_json.clone()
1963    }
1964
1965    /// Returns the stable region size in bytes.
1966    #[napi(getter)]
1967    pub fn stable_size(&self) -> u32 {
1968        self.stable_size
1969    }
1970
1971    /// Returns the total memory size in bytes.
1972    #[napi(getter)]
1973    pub fn total_size(&self) -> u32 {
1974        self.total_size
1975    }
1976
1977    /// Returns the WASM module bytes for eval_comb (combinational logic evaluation).
1978    #[napi]
1979    pub fn comb_wasm_bytes(&self) -> Vec<u8> {
1980        let wasm = celox::wasm_codegen::compile_units(
1981            &self.program.sir.eval_comb,
1982            self.program.layout(),
1983            self.four_state,
1984            false,
1985        );
1986        wasm.bytes
1987    }
1988
1989    /// Returns the WASM module bytes for a specific clock/reset event.
1990    ///
1991    /// `event_name` should match a clock or reset port name (e.g. "clk", "rst").
1992    #[napi]
1993    pub fn event_wasm_bytes(&self, event_name: String) -> Result<Vec<u8>> {
1994        for (addr, units) in &self.program.sir.eval_apply_ffs {
1995            let event_path = self.program.get_path(addr);
1996            if event_path == event_name {
1997                let wasm = celox::wasm_codegen::compile_units(
1998                    units,
1999                    self.program.layout(),
2000                    self.four_state,
2001                    false,
2002                );
2003                return Ok(wasm.bytes);
2004            }
2005        }
2006
2007        Err(Error::from_reason(format!(
2008            "Event '{}' not found. Available events: {}",
2009            event_name,
2010            self.program
2011                .sir
2012                .eval_apply_ffs
2013                .keys()
2014                .map(|addr| self.program.get_path(addr))
2015                .collect::<Vec<_>>()
2016                .join(", ")
2017        )))
2018    }
2019
2020    /// No-op on wasm32 (no native resources to release).
2021    #[napi]
2022    pub fn dispose(&mut self) {}
2023}
2024
2025#[cfg(target_arch = "wasm32")]
2026impl NativeSimulatorHandle {
2027    fn write_memory_bit(memory: &mut [u8], byte: usize, bit: usize, value: bool) {
2028        let mask = 1u8 << bit;
2029        if value {
2030            memory[byte] |= mask;
2031        } else {
2032            memory[byte] &= !mask;
2033        }
2034    }
2035
2036    fn byte_bit(bytes: &[u8], bit: usize) -> bool {
2037        bytes
2038            .get(bit / 8)
2039            .is_some_and(|byte| byte & (1u8 << (bit % 8)) != 0)
2040    }
2041
2042    fn build_initial_memory_bytes(
2043        program: &celox::LaidOutProgram,
2044        layout: &celox::MemoryLayout,
2045        four_state: bool,
2046    ) -> Vec<u8> {
2047        let mut memory = vec![0u8; layout.merged_total_size];
2048
2049        if four_state {
2050            for (address, &offset) in &layout.offsets {
2051                if layout.is_4states.get(address).copied().unwrap_or(false) {
2052                    let plane_size = layout.plane_size(address);
2053                    memory[offset..offset + plane_size * 2].fill(0xff);
2054                }
2055            }
2056            for (address, &relative_offset) in &layout.working_offsets {
2057                if layout.is_4states.get(address).copied().unwrap_or(false) {
2058                    let offset = layout.working_base_offset + relative_offset;
2059                    let plane_size = layout.plane_size(address);
2060                    memory[offset..offset + plane_size * 2].fill(0xff);
2061                }
2062            }
2063        }
2064
2065        for initial in &program.design.initial_state {
2066            let Some(&offset) = layout.offsets.get(&initial.address) else {
2067                continue;
2068            };
2069            let width = layout.widths[&initial.address];
2070            let plane_size = layout.plane_size(&initial.address);
2071            let write_mask = four_state
2072                && layout
2073                    .is_4states
2074                    .get(&initial.address)
2075                    .copied()
2076                    .unwrap_or(false);
2077
2078            match &initial.data {
2079                celox_design::InitialStateData::Packed {
2080                    value,
2081                    mask,
2082                    written_mask,
2083                } => {
2084                    let value = value.to_bytes_le();
2085                    let mask = mask.to_bytes_le();
2086                    let written_mask = written_mask.to_bytes_le();
2087                    for bit in 0..width {
2088                        if !Self::byte_bit(&written_mask, bit) {
2089                            continue;
2090                        }
2091                        let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
2092                        let is_unknown = Self::byte_bit(&mask, bit);
2093                        Self::write_memory_bit(
2094                            &mut memory,
2095                            offset + byte,
2096                            intra,
2097                            Self::byte_bit(&value, bit) && (write_mask || !is_unknown),
2098                        );
2099                        if write_mask {
2100                            Self::write_memory_bit(
2101                                &mut memory,
2102                                offset + plane_size + byte,
2103                                intra,
2104                                is_unknown,
2105                            );
2106                        }
2107                    }
2108                }
2109                celox_design::InitialStateData::Writes(runs) => {
2110                    for run in runs {
2111                        for relative_bit in 0..run.bit_width {
2112                            let bit = run.bit_offset + relative_bit;
2113                            if bit >= width {
2114                                break;
2115                            }
2116                            let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
2117                            Self::write_memory_bit(
2118                                &mut memory,
2119                                offset + byte,
2120                                intra,
2121                                Self::byte_bit(&run.value_bytes, relative_bit),
2122                            );
2123                            if write_mask {
2124                                Self::write_memory_bit(
2125                                    &mut memory,
2126                                    offset + plane_size + byte,
2127                                    intra,
2128                                    Self::byte_bit(&run.mask_bytes, relative_bit),
2129                                );
2130                            }
2131                        }
2132                    }
2133                }
2134            }
2135        }
2136
2137        memory
2138    }
2139
2140    fn build_four_state_init_regions_json(
2141        program: &celox::LaidOutProgram,
2142        layout: &celox::MemoryLayout,
2143        four_state: bool,
2144    ) -> String {
2145        if !four_state {
2146            return "[]".to_string();
2147        }
2148
2149        let mut regions = Vec::new();
2150        for (addr, &offset) in &layout.offsets {
2151            if program
2152                .design
2153                .state_objects
2154                .get(addr)
2155                .is_some_and(|metadata| metadata.is_4state)
2156            {
2157                regions.push((offset, layout.plane_size(addr)));
2158            }
2159        }
2160        for (addr, &relative_offset) in &layout.working_offsets {
2161            if program
2162                .design
2163                .state_objects
2164                .get(addr)
2165                .is_some_and(|metadata| metadata.is_4state)
2166            {
2167                regions.push((
2168                    layout.working_base_offset + relative_offset,
2169                    layout.plane_size(addr),
2170                ));
2171            }
2172        }
2173        regions.sort_unstable();
2174
2175        serde_json::to_string(&regions).unwrap_or_else(|_| "[]".to_string())
2176    }
2177
2178    /// Build signal layout JSON from finalized SIR and MemoryLayout.
2179    /// Mirrors the layout format from celox-wasm.
2180    fn build_layout_json(
2181        program: &celox::LaidOutProgram,
2182        layout: &celox::MemoryLayout,
2183        four_state: bool,
2184    ) -> String {
2185        use std::collections::BTreeMap;
2186
2187        let mut layout_map: BTreeMap<String, serde_json::Value> = BTreeMap::new();
2188
2189        for addr in program.design.state_objects.keys() {
2190            let Some(variable) = program.design.variable(addr) else {
2191                continue;
2192            };
2193            let Some(instance) = program.design.instance(addr.instance_id) else {
2194                continue;
2195            };
2196            if !instance.resolves_path_to(&variable.path, *addr) {
2197                continue;
2198            }
2199            let metadata = &program.design.state_objects[addr];
2200            let Some(&offset) = layout.offsets.get(addr) else {
2201                continue;
2202            };
2203            let name = program.get_path(addr);
2204            let total_width = layout.widths.get(addr).copied().unwrap_or(0);
2205            let (width, array_dims) = if metadata.array_dims.is_empty() {
2206                (total_width, None)
2207            } else {
2208                let element_count = metadata.array_dims.iter().product::<usize>();
2209                (total_width / element_count, Some(&metadata.array_dims))
2210            };
2211            let byte_size = celox::get_byte_size(width);
2212            let mut entry = serde_json::json!({
2213                "offset": offset,
2214                "width": width,
2215                "byte_size": byte_size,
2216                "is_4state": four_state && metadata.is_4state,
2217                "direction": layout::direction_str(variable.var_kind),
2218                "type_kind": layout::type_kind_str(metadata.type_kind),
2219            });
2220            if let Some(array_dims) = array_dims {
2221                entry["array_dims"] = serde_json::json!(array_dims);
2222            }
2223            layout_map.insert(name, entry);
2224        }
2225
2226        serde_json::to_string(&layout_map).unwrap_or_else(|_| "{}".to_string())
2227    }
2228
2229    /// Build events JSON from finalized SIR.
2230    fn build_events_json(program: &celox::LaidOutProgram) -> String {
2231        use std::collections::BTreeMap;
2232
2233        let mut events: BTreeMap<String, usize> = BTreeMap::new();
2234
2235        for (next_id, addr) in program.sir.eval_apply_ffs.keys().enumerate() {
2236            let name = program.get_path(addr);
2237            events.insert(name, next_id);
2238        }
2239
2240        serde_json::to_string(&events).unwrap_or_else(|_| "{}".to_string())
2241    }
2242}
2243
2244/// Convert an `AnalyzerError` to structured `JsonDiagnostic`s.
2245/// `all_sources` maps path → source content for offset → line:col conversion.
2246fn analyzer_error_to_diagnostics(
2247    err: &veryl_analyzer::AnalyzerError,
2248    all_sources: &[(String, String)],
2249    is_error: bool,
2250) -> Vec<celox_ts_gen::JsonDiagnostic> {
2251    use miette::Diagnostic as _;
2252
2253    let severity = if is_error {
2254        celox_ts_gen::DiagnosticSeverity::Error
2255    } else {
2256        celox_ts_gen::DiagnosticSeverity::Warning
2257    };
2258
2259    let message = format!("{err}");
2260    let help = err.help().map(|h| h.to_string());
2261    let url = err.url().map(|u| u.to_string());
2262
2263    let labels: Vec<_> = err.labels().map(|l| l.collect()).unwrap_or_default();
2264
2265    if labels.is_empty() {
2266        return vec![celox_ts_gen::JsonDiagnostic {
2267            severity,
2268            message,
2269            file: String::new(),
2270            line: 1,
2271            column: 1,
2272            end_line: None,
2273            end_column: None,
2274            help,
2275            url,
2276        }];
2277    }
2278
2279    labels
2280        .into_iter()
2281        .map(|label| {
2282            let offset = label.offset();
2283            let len = label.len();
2284
2285            // Find which source file this offset belongs to
2286            // AnalyzerError uses global offsets across concatenated sources,
2287            // but typically the error_location is within a single file.
2288            // Try each source to find line:col.
2289            let mut file = String::new();
2290            let mut line = 1usize;
2291            let mut col = 1usize;
2292            let mut end_line = None;
2293            let mut end_col = None;
2294
2295            for (path, content) in all_sources {
2296                // miette offsets are per-file when Parser::parse is given the source
2297                let mut cur_line = 1;
2298                let mut cur_col = 1;
2299                let mut found = false;
2300
2301                for (i, ch) in content.char_indices() {
2302                    if i == offset {
2303                        file = path.clone();
2304                        line = cur_line;
2305                        col = cur_col;
2306                        found = true;
2307                    }
2308                    if found && i == offset + len {
2309                        end_line = Some(cur_line);
2310                        end_col = Some(cur_col);
2311                        break;
2312                    }
2313                    if ch == '\n' {
2314                        cur_line += 1;
2315                        cur_col = 1;
2316                    } else {
2317                        cur_col += 1;
2318                    }
2319                }
2320
2321                if found {
2322                    if end_line.is_none() {
2323                        end_line = Some(cur_line);
2324                        end_col = Some(cur_col);
2325                    }
2326                    break;
2327                }
2328            }
2329
2330            celox_ts_gen::JsonDiagnostic {
2331                severity: severity.clone(),
2332                message: label.label().unwrap_or(&message).to_string(),
2333                file,
2334                line,
2335                column: col,
2336                end_line,
2337                end_column: end_col,
2338                help: help.clone(),
2339                url: url.clone(),
2340            }
2341        })
2342        .collect()
2343}
2344
2345/// Format analyzer errors with accumulated warnings for gen_ts error messages.
2346fn format_errors_with_warnings(
2347    pass_label: &str,
2348    errors: &[&veryl_analyzer::AnalyzerError],
2349    warnings: &[veryl_analyzer::AnalyzerError],
2350) -> String {
2351    let error_msgs: Vec<String> = errors
2352        .iter()
2353        .map(|e| celox::render_diagnostic(*e))
2354        .collect();
2355    let mut msg = format!("Errors in {pass_label}: {}", error_msgs.join("; "));
2356    if !warnings.is_empty() {
2357        let warning_msgs: Vec<String> = warnings
2358            .iter()
2359            .map(|w| celox::render_diagnostic(w))
2360            .collect();
2361        msg.push_str("\n\n--- warnings ---\n\n");
2362        msg.push_str(&warning_msgs.join("\n"));
2363    }
2364    msg
2365}
2366
2367/// Clear the process-global JIT compilation cache.
2368///
2369/// Call this when source files have changed and cached compiled code may be stale.
2370#[cfg(not(target_arch = "wasm32"))]
2371#[napi]
2372pub fn clear_jit_cache() {
2373    let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
2374    cache.clear();
2375}
2376
2377/// Stub for wasm32: no JIT cache to clear.
2378#[cfg(target_arch = "wasm32")]
2379#[napi]
2380pub fn clear_jit_cache() {}
2381
2382// ---------------------------------------------------------------------------
2383//  Native testbench execution
2384// ---------------------------------------------------------------------------
2385
2386/// Result of a single `$assert` evaluation in a native testbench.
2387#[cfg(not(target_arch = "wasm32"))]
2388#[napi(object)]
2389pub struct NapiAssertionResult {
2390    pub passed: bool,
2391    pub message: Option<String>,
2392    pub file: Option<String>,
2393    pub line: Option<u32>,
2394    pub column: Option<u32>,
2395}
2396
2397/// Detailed result of running a native testbench.
2398#[cfg(not(target_arch = "wasm32"))]
2399#[napi(object)]
2400pub struct NapiTestResult {
2401    pub passed: bool,
2402    pub assertions: Vec<NapiAssertionResult>,
2403    pub error: Option<String>,
2404}
2405
2406#[cfg(not(target_arch = "wasm32"))]
2407fn convert_test_result(r: celox::TestResultDetailed) -> NapiTestResult {
2408    NapiTestResult {
2409        passed: r.passed,
2410        error: r.error,
2411        assertions: r
2412            .assertions
2413            .into_iter()
2414            .map(|a| {
2415                let (file, line, column) = match a.location {
2416                    Some(loc) => (Some(loc.file), Some(loc.line), Some(loc.column)),
2417                    None => (None, None, None),
2418                };
2419                NapiAssertionResult {
2420                    passed: a.passed,
2421                    message: a.message,
2422                    file,
2423                    line,
2424                    column,
2425                }
2426            })
2427            .collect(),
2428    }
2429}
2430
2431#[cfg(not(target_arch = "wasm32"))]
2432#[napi(object)]
2433pub struct NapiInjectedValue {
2434    pub name: Option<String>,
2435    pub bits: Option<BigInt>,
2436    pub mask_xz: Option<BigInt>,
2437    pub width: Option<u32>,
2438    pub string_value: Option<String>,
2439}
2440
2441#[cfg(not(target_arch = "wasm32"))]
2442#[napi(object)]
2443pub struct NapiInjectedCall {
2444    pub instance: String,
2445    pub phase: String,
2446    pub method: Option<String>,
2447    pub inputs: Vec<NapiInjectedValue>,
2448    pub params: Vec<NapiInjectedValue>,
2449    pub ports: Vec<NapiInjectedPort>,
2450    pub args: Vec<NapiInjectedValue>,
2451    pub cycle: BigInt,
2452    pub time: BigInt,
2453    pub seed: BigInt,
2454    pub fired_clock: Option<String>,
2455    pub four_state: bool,
2456}
2457
2458#[cfg(not(target_arch = "wasm32"))]
2459#[napi(object)]
2460pub struct NapiInjectedPort {
2461    pub name: String,
2462    pub direction: String,
2463    pub role: Option<String>,
2464    pub width: u32,
2465}
2466
2467#[cfg(not(target_arch = "wasm32"))]
2468#[napi(object)]
2469pub struct NapiInjectedResult {
2470    pub outputs: Option<Vec<NapiInjectedValue>>,
2471    pub return_value: Option<NapiInjectedValue>,
2472    pub failures: Option<Vec<String>>,
2473    pub logs: Option<Vec<String>>,
2474    pub finish: Option<bool>,
2475}
2476
2477#[cfg(not(target_arch = "wasm32"))]
2478#[napi(object, object_to_js = false)]
2479pub struct NapiInjectedComponent {
2480    pub name: String,
2481    pub manifest: String,
2482    pub handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2483}
2484
2485#[cfg(not(target_arch = "wasm32"))]
2486struct NapiInjectedHandler {
2487    env: Env,
2488    handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2489}
2490
2491// Injected callbacks are only accepted by the synchronous runTest APIs and
2492// are invoked on the JS thread which supplied this Env. The core trait is
2493// Send + Sync because compiled component hooks may otherwise be movable.
2494#[cfg(not(target_arch = "wasm32"))]
2495unsafe impl Send for NapiInjectedHandler {}
2496#[cfg(not(target_arch = "wasm32"))]
2497unsafe impl Sync for NapiInjectedHandler {}
2498
2499#[cfg(not(target_arch = "wasm32"))]
2500fn napi_bigint(value: u64) -> BigInt {
2501    BigInt {
2502        sign_bit: false,
2503        words: vec![value],
2504    }
2505}
2506
2507#[cfg(not(target_arch = "wasm32"))]
2508fn to_napi_injected_value(name: Option<String>, value: celox::InjectedValue) -> NapiInjectedValue {
2509    match value {
2510        celox::InjectedValue::Bits {
2511            words,
2512            mask_xz,
2513            width,
2514        } => NapiInjectedValue {
2515            name,
2516            bits: Some(BigInt {
2517                sign_bit: false,
2518                words,
2519            }),
2520            mask_xz: Some(BigInt {
2521                sign_bit: false,
2522                words: mask_xz,
2523            }),
2524            width: Some(width),
2525            string_value: None,
2526        },
2527        celox::InjectedValue::String(value) => NapiInjectedValue {
2528            name,
2529            bits: None,
2530            mask_xz: None,
2531            width: None,
2532            string_value: Some(value),
2533        },
2534        celox::InjectedValue::Unit => NapiInjectedValue {
2535            name,
2536            bits: None,
2537            mask_xz: None,
2538            width: None,
2539            string_value: None,
2540        },
2541    }
2542}
2543
2544#[cfg(not(target_arch = "wasm32"))]
2545fn from_napi_injected_value(
2546    value: NapiInjectedValue,
2547) -> std::result::Result<celox::InjectedValue, String> {
2548    if let Some(bits) = value.bits {
2549        if bits.sign_bit || value.mask_xz.as_ref().is_some_and(|mask| mask.sign_bit) {
2550            return Err("component callback values cannot be negative".into());
2551        }
2552        let width = value
2553            .width
2554            .ok_or_else(|| "component callback bit value has no width".to_string())?;
2555        return Ok(celox::InjectedValue::Bits {
2556            words: bits.words,
2557            mask_xz: value.mask_xz.map(|mask| mask.words).unwrap_or_default(),
2558            width,
2559        });
2560    }
2561    Ok(match value.string_value {
2562        Some(value) => celox::InjectedValue::String(value),
2563        None => celox::InjectedValue::Unit,
2564    })
2565}
2566
2567#[cfg(not(target_arch = "wasm32"))]
2568impl celox::InjectedComponentHandler for NapiInjectedHandler {
2569    fn call(
2570        &self,
2571        call: celox::InjectedCall,
2572    ) -> std::result::Result<celox::InjectedResult, String> {
2573        let (phase, method, args) = match call.hook {
2574            celox::InjectedHook::Create => ("create", None, Vec::new()),
2575            celox::InjectedHook::Init => ("init", None, Vec::new()),
2576            celox::InjectedHook::Reset => ("reset", None, Vec::new()),
2577            celox::InjectedHook::Clock => ("clock", None, Vec::new()),
2578            celox::InjectedHook::Finish => ("finish", None, Vec::new()),
2579            celox::InjectedHook::Method { name, args } => ("method", Some(name), args),
2580        };
2581        let request = NapiInjectedCall {
2582            instance: call.instance,
2583            phase: phase.into(),
2584            method,
2585            inputs: call
2586                .inputs
2587                .into_iter()
2588                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2589                .collect(),
2590            params: call
2591                .params
2592                .into_iter()
2593                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2594                .collect(),
2595            ports: call
2596                .ports
2597                .into_iter()
2598                .map(|port| NapiInjectedPort {
2599                    name: port.name,
2600                    direction: port.direction,
2601                    role: port.role,
2602                    width: port.width,
2603                })
2604                .collect(),
2605            args: args
2606                .into_iter()
2607                .map(|value| to_napi_injected_value(None, value))
2608                .collect(),
2609            cycle: napi_bigint(call.cycle),
2610            time: napi_bigint(call.time),
2611            seed: napi_bigint(call.seed),
2612            fired_clock: call.fired_clock,
2613            four_state: call.four_state,
2614        };
2615        let result = self
2616            .handler
2617            .borrow_back(&self.env)
2618            .and_then(|handler| handler.call(request))
2619            .map_err(|error| error.to_string())?;
2620        let outputs = result
2621            .outputs
2622            .unwrap_or_default()
2623            .into_iter()
2624            .map(|value| {
2625                let name = value
2626                    .name
2627                    .clone()
2628                    .ok_or_else(|| "component callback output has no name".to_string())?;
2629                Ok(celox::InjectedNamedValue {
2630                    name,
2631                    value: from_napi_injected_value(value)?,
2632                })
2633            })
2634            .collect::<std::result::Result<Vec<_>, String>>()?;
2635        Ok(celox::InjectedResult {
2636            outputs,
2637            return_value: result
2638                .return_value
2639                .map(from_napi_injected_value)
2640                .transpose()?,
2641            failures: result.failures.unwrap_or_default(),
2642            logs: result.logs.unwrap_or_default(),
2643            finish: result.finish.unwrap_or(false),
2644        })
2645    }
2646}
2647
2648#[cfg(not(target_arch = "wasm32"))]
2649fn injected_components(
2650    env: Env,
2651    definitions: Option<Vec<NapiInjectedComponent>>,
2652) -> Result<celox::InjectedComponents> {
2653    let mut components = celox::InjectedComponents::new();
2654    for definition in definitions.unwrap_or_default() {
2655        components
2656            .insert(
2657                definition.name,
2658                &definition.manifest,
2659                Arc::new(NapiInjectedHandler {
2660                    env,
2661                    handler: definition.handler,
2662                }),
2663            )
2664            .map_err(Error::from_reason)?;
2665    }
2666    Ok(components)
2667}
2668
2669/// Run a native testbench from Veryl source code.
2670///
2671/// Compiles the given sources and runs the `#[test]` module specified by `top`,
2672/// returning assertion results observed before that test finishes or stops on a
2673/// fatal failure.
2674#[cfg(not(target_arch = "wasm32"))]
2675#[napi]
2676pub fn run_test(
2677    env: Env,
2678    sources: Vec<NapiSourceFile>,
2679    top: String,
2680    options: Option<NapiOptions>,
2681    components: Option<Vec<NapiInjectedComponent>>,
2682) -> Result<NapiTestResult> {
2683    let opts = parse_options(&options)?;
2684    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
2685        .into_iter()
2686        .map(|s| (s.content, std::path::PathBuf::from(s.path)))
2687        .collect();
2688    append_extra_source(&mut src_pairs, &opts.extra_source);
2689
2690    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
2691        .iter()
2692        .map(|(s, p)| (s.as_str(), p.as_path()))
2693        .collect();
2694    let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts)
2695        .with_injected_components(injected_components(env, components)?);
2696    let result = builder
2697        .run_test_detailed()
2698        .map_err(|e| Error::from_reason(format!("{e}")))?;
2699    Ok(convert_test_result(result))
2700}
2701
2702/// Run a Veryl native testbench against an external frontend artifact.
2703#[cfg(not(target_arch = "wasm32"))]
2704#[napi]
2705pub fn run_test_with_frontend_artifact(
2706    env: Env,
2707    artifact_json: String,
2708    sources: Vec<NapiSourceFile>,
2709    top: String,
2710    options: Option<NapiOptions>,
2711    components: Option<Vec<NapiInjectedComponent>>,
2712) -> Result<NapiTestResult> {
2713    let opts = parse_options(&options)?;
2714    let artifact = celox::FrontendArtifact::from_json(&artifact_json)
2715        .map_err(|error| Error::from_reason(error.to_string()))?;
2716    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
2717        .into_iter()
2718        .map(|source| (source.content, std::path::PathBuf::from(source.path)))
2719        .collect();
2720    append_extra_source(&mut src_pairs, &opts.extra_source);
2721    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
2722        .iter()
2723        .map(|(source, path)| (source.as_str(), path.as_path()))
2724        .collect();
2725    let builder = apply_options(
2726        celox::Simulator::from_frontend_with_testbench(artifact, source_refs, &top),
2727        &opts,
2728    )
2729    .with_injected_components(injected_components(env, components)?);
2730    let result = builder
2731        .run_test_detailed()
2732        .map_err(|error| Error::from_reason(error.to_string()))?;
2733    Ok(convert_test_result(result))
2734}
2735
2736/// Run a native testbench from a Veryl project directory.
2737///
2738/// Searches upward from `project_path` for `Veryl.toml`, gathers all
2739/// `.veryl` source files, and runs the `#[test]` module specified by `top`.
2740#[cfg(not(target_arch = "wasm32"))]
2741#[napi]
2742pub fn run_test_from_project(
2743    env: Env,
2744    project_path: String,
2745    top: String,
2746    options: Option<NapiOptions>,
2747    components: Option<Vec<NapiInjectedComponent>>,
2748) -> Result<NapiTestResult> {
2749    let opts = parse_options(&options)?;
2750    let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
2751    append_extra_source(&mut sources, &opts.extra_source);
2752
2753    let source_refs: Vec<(&str, &std::path::Path)> = sources
2754        .iter()
2755        .map(|(s, p)| (s.as_str(), p.as_path()))
2756        .collect();
2757    let builder = apply_options(
2758        celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
2759        &opts,
2760    )
2761    .with_injected_components(injected_components(env, components)?);
2762    let result = builder
2763        .run_test_detailed()
2764        .map_err(|e| Error::from_reason(format!("{e}")))?;
2765    Ok(convert_test_result(result))
2766}
2767
2768// ---------------------------------------------------------------------------
2769//  TypeScript type generation
2770// ---------------------------------------------------------------------------
2771
2772#[napi(object)]
2773pub struct NapiInjectedManifest {
2774    pub name: String,
2775    pub manifest: String,
2776}
2777
2778fn inject_analyzer_components(definitions: Option<Vec<NapiInjectedManifest>>) -> Result<()> {
2779    let definitions = definitions.unwrap_or_default();
2780    let names: Vec<_> = definitions
2781        .iter()
2782        .map(|definition| definition.name.as_str())
2783        .collect();
2784    veryl_analyzer::tb_component::insert_external_components(&names);
2785    for definition in definitions {
2786        let manifest =
2787            veryl_metadata::ComponentManifest::parse(&definition.manifest).ok_or_else(|| {
2788                Error::from_reason(format!(
2789                    "manifest of injected component `{}` cannot be parsed",
2790                    definition.name
2791                ))
2792            })?;
2793        veryl_analyzer::component_manifest_table::insert(
2794            veryl_parser::resource_table::insert_str(&definition.name),
2795            manifest,
2796        );
2797    }
2798    Ok(())
2799}
2800
2801/// Generate TypeScript type information as JSON for a Veryl project.
2802///
2803/// Equivalent to running `celox-gen-ts --json` from the given project directory.
2804#[napi]
2805pub fn gen_ts(
2806    project_path: String,
2807    components: Option<Vec<NapiInjectedManifest>>,
2808) -> Result<String> {
2809    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
2810
2811    let toml_path = Metadata::search_from(&project_path)
2812        .map_err(|e| Error::from_reason(format!("Could not find Veryl.toml: {e}")))?;
2813    let mut metadata = Metadata::load(&toml_path)
2814        .map_err(|e| Error::from_reason(format!("Failed to load Veryl.toml: {e}")))?;
2815
2816    let base_path = toml_path
2817        .parent()
2818        .unwrap_or(&toml_path)
2819        .to_string_lossy()
2820        .to_string();
2821
2822    let mut paths = metadata
2823        .paths::<std::path::PathBuf>(&[], true, true)
2824        .map_err(|e| Error::from_reason(format!("Failed to gather sources: {e}")))?;
2825    paths.retain(|path| !path.example);
2826
2827    // Append test-only sources declared in celox.toml
2828    let project_root = toml_path.parent().unwrap_or(&toml_path).to_path_buf();
2829    let celox_cfg = load_celox_config(&project_root)?;
2830    let prj_name = metadata.project.name.clone();
2831    for dir in &celox_cfg.test.sources {
2832        let dir_path = project_root.join(dir);
2833        if !dir_path.exists() {
2834            continue;
2835        }
2836        for src in walkdir(&dir_path)? {
2837            paths.push(PathSet {
2838                prj: prj_name.clone(),
2839                src: src.clone(),
2840                dst: src.with_extension("sv"),
2841                map: src.with_extension("map"),
2842                example: false,
2843            });
2844        }
2845    }
2846
2847    if let Some(exclude_set) = build_exclude_set(&celox_cfg)? {
2848        paths.retain(|p| !is_excluded(&p.src, &project_root, &exclude_set));
2849    }
2850
2851    if paths.is_empty() {
2852        return Err(Error::from_reason("No Veryl source files found"));
2853    }
2854
2855    // Parse and analyze pass 1
2856    symbol_table::clear();
2857    attribute_table::clear();
2858
2859    let analyzer = Analyzer::new(&metadata);
2860    inject_analyzer_components(components)?;
2861    let mut parsers = Vec::new();
2862    let mut all_warnings = Vec::new();
2863
2864    for path in &paths {
2865        let input = std::fs::read_to_string(&path.src)
2866            .map_err(|e| Error::from_reason(format!("{}: {e}", path.src.display())))?;
2867        let parser = Parser::parse(&input, &path.src)
2868            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
2869
2870        let results = analyzer.analyze_pass1(&path.prj, &parser.veryl);
2871        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2872        if !real_errors.is_empty() {
2873            return Err(Error::from_reason(format_errors_with_warnings(
2874                "analysis pass 1",
2875                &real_errors,
2876                &all_warnings,
2877            )));
2878        }
2879        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2880
2881        parsers.push((path.clone(), parser));
2882    }
2883
2884    let results = Analyzer::analyze_post_pass1();
2885    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2886    if !real_errors.is_empty() {
2887        return Err(Error::from_reason(format_errors_with_warnings(
2888            "post-pass 1 analysis",
2889            &real_errors,
2890            &all_warnings,
2891        )));
2892    }
2893    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2894
2895    // Pass 2: per-file IR → generate
2896
2897    // Compute all source file relative paths for embedding in generated JS.
2898    let base_normalized = base_path.replace('\\', "/");
2899    let all_source_files: Vec<String> = parsers
2900        .iter()
2901        .map(|(path, _)| {
2902            let src_normalized = path
2903                .src
2904                .to_string_lossy()
2905                .replace(r"\\?\", "")
2906                .replace('\\', "/");
2907            src_normalized
2908                .strip_prefix(&base_normalized)
2909                .unwrap_or(&src_normalized)
2910                .trim_start_matches('/')
2911                .to_string()
2912        })
2913        .collect();
2914    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
2915
2916    let mut all_modules = Vec::new();
2917    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
2918    let mut post_pass_ir = Ir::default();
2919
2920    for (i, (_path, parser)) in parsers.iter().enumerate() {
2921        let mut analyzer_context = Context::default();
2922        let mut ir = Ir::default();
2923        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
2924        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2925        if !real_errors.is_empty() {
2926            return Err(Error::from_reason(format_errors_with_warnings(
2927                "analysis pass 2",
2928                &real_errors,
2929                &all_warnings,
2930            )));
2931        }
2932        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2933
2934        let modules = generate_all(&ir, &source_file_refs);
2935        post_pass_ir.append(&mut ir);
2936        let source_file = all_source_files[i].clone();
2937
2938        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
2939        if !module_names.is_empty() {
2940            file_modules.insert(source_file.clone(), module_names);
2941        }
2942
2943        for m in modules {
2944            all_modules.push(JsonModuleEntry {
2945                module_name: m.module_name,
2946                source_file: source_file.clone(),
2947                dts_content: m.dts_content,
2948                md_content: m.md_content,
2949                ports: m.ports,
2950                events: m.events,
2951                instances: m.instances,
2952                is_test: m.is_test,
2953            });
2954        }
2955    }
2956
2957    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
2958    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
2959    if !real_errors.is_empty() {
2960        return Err(Error::from_reason(format_errors_with_warnings(
2961            "post-pass 2 analysis",
2962            &real_errors,
2963            &all_warnings,
2964        )));
2965    }
2966    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
2967
2968    // Sort for deterministic output
2969    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
2970
2971    let warning_msgs: Vec<String> = all_warnings
2972        .iter()
2973        .map(|w| celox::render_diagnostic(w))
2974        .collect();
2975
2976    let all_sources: Vec<(String, String)> = parsers
2977        .iter()
2978        .map(|(p, _)| {
2979            let path_str = p.src.to_string_lossy().to_string();
2980            let content = std::fs::read_to_string(&p.src).unwrap_or_default();
2981            (path_str, content)
2982        })
2983        .collect();
2984
2985    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
2986        .iter()
2987        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
2988        .collect();
2989
2990    let output = JsonOutput {
2991        project_path: base_path,
2992        modules: all_modules,
2993        file_modules,
2994        warnings: warning_msgs,
2995        diagnostics,
2996    };
2997
2998    serde_json::to_string(&output)
2999        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
3000}
3001
3002/// Generate TypeScript type information from in-memory Veryl sources.
3003///
3004/// Like `gen_ts()` but does not require a Veryl.toml or filesystem access.
3005/// Works on both native and wasm32 targets.
3006#[napi]
3007pub fn gen_ts_from_source(
3008    sources: Vec<NapiSourceFile>,
3009    components: Option<Vec<NapiInjectedManifest>>,
3010) -> Result<String> {
3011    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
3012
3013    if sources.is_empty() {
3014        return Err(Error::from_reason("No source files provided"));
3015    }
3016
3017    let metadata = Metadata::create_default("playground")
3018        .map_err(|e| Error::from_reason(format!("Failed to create default metadata: {e}")))?;
3019
3020    // Parse and analyze pass 1
3021    symbol_table::clear();
3022    attribute_table::clear();
3023
3024    let analyzer = Analyzer::new(&metadata);
3025    inject_analyzer_components(components)?;
3026    let mut parsers = Vec::new();
3027    let mut all_warnings = Vec::new();
3028
3029    for src in &sources {
3030        let path = std::path::PathBuf::from(&src.path);
3031        let parser = Parser::parse(&src.content, &path)
3032            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
3033
3034        let prj = "playground".to_string();
3035        let results = analyzer.analyze_pass1(&prj, &parser.veryl);
3036        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3037        if !real_errors.is_empty() {
3038            return Err(Error::from_reason(format_errors_with_warnings(
3039                "analysis pass 1",
3040                &real_errors,
3041                &all_warnings,
3042            )));
3043        }
3044        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3045
3046        parsers.push((prj, path, parser));
3047    }
3048
3049    let results = Analyzer::analyze_post_pass1();
3050    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3051    if !real_errors.is_empty() {
3052        return Err(Error::from_reason(format_errors_with_warnings(
3053            "post-pass 1 analysis",
3054            &real_errors,
3055            &all_warnings,
3056        )));
3057    }
3058    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3059
3060    // Pass 2: per-file IR → generate
3061    let all_source_files: Vec<String> = parsers
3062        .iter()
3063        .map(|(_, p, _)| p.to_string_lossy().replace('\\', "/"))
3064        .collect();
3065    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
3066
3067    let mut all_modules = Vec::new();
3068    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
3069    let mut post_pass_ir = Ir::default();
3070
3071    for (i, (_prj, _path, parser)) in parsers.iter().enumerate() {
3072        let mut analyzer_context = Context::default();
3073        let mut ir = Ir::default();
3074        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
3075        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3076        if !real_errors.is_empty() {
3077            return Err(Error::from_reason(format_errors_with_warnings(
3078                "analysis pass 2",
3079                &real_errors,
3080                &all_warnings,
3081            )));
3082        }
3083        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3084
3085        let modules = generate_all(&ir, &source_file_refs);
3086        post_pass_ir.append(&mut ir);
3087        let source_file = all_source_files[i].clone();
3088
3089        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
3090        if !module_names.is_empty() {
3091            file_modules.insert(source_file.clone(), module_names);
3092        }
3093
3094        for m in modules {
3095            all_modules.push(JsonModuleEntry {
3096                module_name: m.module_name,
3097                source_file: source_file.clone(),
3098                dts_content: m.dts_content,
3099                md_content: m.md_content,
3100                ports: m.ports,
3101                events: m.events,
3102                instances: m.instances,
3103                is_test: m.is_test,
3104            });
3105        }
3106    }
3107
3108    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
3109    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3110    if !real_errors.is_empty() {
3111        return Err(Error::from_reason(format_errors_with_warnings(
3112            "post-pass 2 analysis",
3113            &real_errors,
3114            &all_warnings,
3115        )));
3116    }
3117    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3118
3119    // Sort for deterministic output
3120    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
3121
3122    let warning_msgs: Vec<String> = all_warnings
3123        .iter()
3124        .map(|w| celox::render_diagnostic(w))
3125        .collect();
3126
3127    let all_sources: Vec<(String, String)> = sources
3128        .iter()
3129        .map(|s| (s.path.clone(), s.content.clone()))
3130        .collect();
3131
3132    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
3133        .iter()
3134        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
3135        .collect();
3136
3137    let output = JsonOutput {
3138        project_path: String::new(),
3139        modules: all_modules,
3140        file_modules,
3141        warnings: warning_msgs,
3142        diagnostics,
3143    };
3144
3145    serde_json::to_string(&output)
3146        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
3147}
3148
3149#[cfg(all(test, not(target_arch = "wasm32")))]
3150mod tests {
3151    use super::*;
3152
3153    fn default_opts() -> ParsedOptions {
3154        ParsedOptions {
3155            common: ParsedOptionsCommon {
3156                four_state: false,
3157                optimize_options: celox::OptimizeOptions::all(),
3158                vcd: None,
3159                false_loops: vec![],
3160                true_loops: vec![],
3161                clock_type: None,
3162                reset_type: None,
3163                extra_source: None,
3164                parameters: vec![],
3165            },
3166            cranelift_options: celox::CraneliftOptions::default(),
3167            dead_store_policy: celox::DeadStorePolicy::Off,
3168        }
3169    }
3170
3171    fn make_sources(pairs: &[(&str, &str)]) -> Vec<(String, std::path::PathBuf)> {
3172        pairs
3173            .iter()
3174            .map(|(content, path)| (content.to_string(), std::path::PathBuf::from(path)))
3175            .collect()
3176    }
3177
3178    #[test]
3179    fn simulator_handle_derives_four_state_metadata_from_layout() {
3180        let source = "module Top (a: input logic<1>) {}";
3181        let path = std::path::Path::new("top.veryl");
3182
3183        for four_state in [false, true] {
3184            let simulator = celox::Simulator::from_sources(vec![(source, path)], "Top")
3185                .four_state(four_state)
3186                .build()
3187                .unwrap();
3188            let handle = NativeSimulatorHandle::from_simulator(simulator, None).unwrap();
3189            let layout: serde_json::Value = serde_json::from_str(&handle.layout_json()).unwrap();
3190
3191            assert_eq!(layout["a"]["is_4state"].as_bool(), Some(four_state));
3192        }
3193    }
3194
3195    #[test]
3196    fn normal_project_loading_excludes_example_sources() {
3197        let project = tempfile::tempdir().unwrap();
3198        std::fs::create_dir(project.path().join("src")).unwrap();
3199        std::fs::create_dir(project.path().join("examples")).unwrap();
3200        std::fs::write(
3201            project.path().join("Veryl.toml"),
3202            "[project]\nname = \"example_filter\"\nversion = \"0.1.0\"\n",
3203        )
3204        .unwrap();
3205        std::fs::write(project.path().join("src/top.veryl"), "module Top () {}\n").unwrap();
3206        std::fs::write(
3207            project.path().join("examples/demo.veryl"),
3208            "module Demo () {}\n",
3209        )
3210        .unwrap();
3211
3212        let (sources, _, _) = load_project_sources(project.path().to_str().unwrap()).unwrap();
3213        let source_names = sources
3214            .iter()
3215            .map(|(_, path)| path.file_name().unwrap().to_str().unwrap())
3216            .collect::<Vec<_>>();
3217        assert_eq!(source_names, ["top.veryl"]);
3218    }
3219
3220    #[test]
3221    fn same_inputs_produce_same_key() {
3222        let src = make_sources(&[("module Top {}", "a.veryl")]);
3223        let opts = default_opts();
3224        let k1 = build_cache_key(&src, "Top", &opts, None);
3225        let k2 = build_cache_key(&src, "Top", &opts, None);
3226        assert_eq!(k1, k2);
3227    }
3228
3229    #[test]
3230    fn high_index_sir_pass_changes_cache_key_without_bit_packing() {
3231        let src = make_sources(&[("module Top {}", "a.veryl")]);
3232        let enabled = default_opts();
3233        let mut disabled = default_opts();
3234        disabled.common.optimize_options = disabled
3235            .common
3236            .optimize_options
3237            .clone()
3238            .disable(celox::SirPass::IdentityStoreBypass);
3239
3240        assert_ne!(
3241            build_cache_key(&src, "Top", &enabled, None),
3242            build_cache_key(&src, "Top", &disabled, None)
3243        );
3244    }
3245
3246    #[test]
3247    fn native_memory_width_changes_cache_key() {
3248        let src = make_sources(&[("module Top {}", "a.veryl")]);
3249        let mut narrow = default_opts();
3250        narrow.common.optimize_options = narrow
3251            .common
3252            .optimize_options
3253            .clone()
3254            .with_max_native_memory_width(64);
3255        let mut wide = default_opts();
3256        wide.common.optimize_options = wide
3257            .common
3258            .optimize_options
3259            .clone()
3260            .with_max_native_memory_width(128);
3261
3262        assert_ne!(
3263            build_cache_key(&src, "Top", &narrow, None),
3264            build_cache_key(&src, "Top", &wide, None)
3265        );
3266    }
3267
3268    #[test]
3269    fn different_source_content_different_key() {
3270        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3271        let s2 = make_sources(&[("module B {}", "a.veryl")]);
3272        let opts = default_opts();
3273        assert_ne!(
3274            build_cache_key(&s1, "Top", &opts, None),
3275            build_cache_key(&s2, "Top", &opts, None),
3276        );
3277    }
3278
3279    #[test]
3280    fn different_source_path_different_key() {
3281        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3282        let s2 = make_sources(&[("module A {}", "b.veryl")]);
3283        let opts = default_opts();
3284        assert_ne!(
3285            build_cache_key(&s1, "Top", &opts, None),
3286            build_cache_key(&s2, "Top", &opts, None),
3287        );
3288    }
3289
3290    #[test]
3291    fn different_top_different_key() {
3292        let src = make_sources(&[("module Top {}", "a.veryl")]);
3293        let opts = default_opts();
3294        assert_ne!(
3295            build_cache_key(&src, "Top", &opts, None),
3296            build_cache_key(&src, "Other", &opts, None),
3297        );
3298    }
3299
3300    #[test]
3301    fn four_state_differs() {
3302        let src = make_sources(&[("module Top {}", "a.veryl")]);
3303        let mut o1 = default_opts();
3304        let mut o2 = default_opts();
3305        o1.common.four_state = false;
3306        o2.common.four_state = true;
3307        assert_ne!(
3308            build_cache_key(&src, "Top", &o1, None),
3309            build_cache_key(&src, "Top", &o2, None),
3310        );
3311    }
3312
3313    #[test]
3314    fn optimize_options_differs() {
3315        let src = make_sources(&[("module Top {}", "a.veryl")]);
3316        let mut o1 = default_opts();
3317        let mut o2 = default_opts();
3318        o1.common.optimize_options = celox::OptimizeOptions::all();
3319        o2.common.optimize_options = celox::OptimizeOptions::none();
3320        assert_ne!(
3321            build_cache_key(&src, "Top", &o1, None),
3322            build_cache_key(&src, "Top", &o2, None),
3323        );
3324    }
3325
3326    #[test]
3327    fn cranelift_opt_level_differs() {
3328        let src = make_sources(&[("module Top {}", "a.veryl")]);
3329        let mut o1 = default_opts();
3330        let mut o2 = default_opts();
3331        o1.cranelift_options.opt_level = celox::CraneliftOptLevel::Speed;
3332        o2.cranelift_options.opt_level = celox::CraneliftOptLevel::None;
3333        assert_ne!(
3334            build_cache_key(&src, "Top", &o1, None),
3335            build_cache_key(&src, "Top", &o2, None),
3336        );
3337    }
3338
3339    #[test]
3340    fn regalloc_algorithm_differs() {
3341        let src = make_sources(&[("module Top {}", "a.veryl")]);
3342        let mut o1 = default_opts();
3343        let mut o2 = default_opts();
3344        o1.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::Backtracking;
3345        o2.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::SinglePass;
3346        assert_ne!(
3347            build_cache_key(&src, "Top", &o1, None),
3348            build_cache_key(&src, "Top", &o2, None),
3349        );
3350    }
3351
3352    #[test]
3353    fn dead_store_policy_differs() {
3354        let src = make_sources(&[("module Top {}", "a.veryl")]);
3355        let mut o1 = default_opts();
3356        let mut o2 = default_opts();
3357        o1.dead_store_policy = celox::DeadStorePolicy::Off;
3358        o2.dead_store_policy = celox::DeadStorePolicy::PreserveTopPorts;
3359        assert_ne!(
3360            build_cache_key(&src, "Top", &o1, None),
3361            build_cache_key(&src, "Top", &o2, None),
3362        );
3363    }
3364
3365    #[test]
3366    fn clock_type_differs() {
3367        let src = make_sources(&[("module Top {}", "a.veryl")]);
3368        let mut o1 = default_opts();
3369        let mut o2 = default_opts();
3370        o1.common.clock_type = None;
3371        o2.common.clock_type = Some(celox::ClockType::NegEdge);
3372        assert_ne!(
3373            build_cache_key(&src, "Top", &o1, None),
3374            build_cache_key(&src, "Top", &o2, None),
3375        );
3376    }
3377
3378    #[test]
3379    fn reset_type_differs() {
3380        let src = make_sources(&[("module Top {}", "a.veryl")]);
3381        let mut o1 = default_opts();
3382        let mut o2 = default_opts();
3383        o1.common.reset_type = None;
3384        o2.common.reset_type = Some(celox::ResetType::SyncHigh);
3385        assert_ne!(
3386            build_cache_key(&src, "Top", &o1, None),
3387            build_cache_key(&src, "Top", &o2, None),
3388        );
3389    }
3390
3391    #[test]
3392    fn parameters_differ() {
3393        let src = make_sources(&[("module Top {}", "a.veryl")]);
3394        let mut o1 = default_opts();
3395        let mut o2 = default_opts();
3396        o1.common.parameters = vec![("WIDTH".into(), 8)];
3397        o2.common.parameters = vec![("WIDTH".into(), 16)];
3398        assert_ne!(
3399            build_cache_key(&src, "Top", &o1, None),
3400            build_cache_key(&src, "Top", &o2, None),
3401        );
3402    }
3403
3404    #[test]
3405    fn source_order_independent() {
3406        let s1 = make_sources(&[("aaa", "a.veryl"), ("bbb", "b.veryl")]);
3407        let s2 = make_sources(&[("bbb", "b.veryl"), ("aaa", "a.veryl")]);
3408        let opts = default_opts();
3409        assert_eq!(
3410            build_cache_key(&s1, "Top", &opts, None),
3411            build_cache_key(&s2, "Top", &opts, None),
3412        );
3413    }
3414
3415    #[test]
3416    fn non_compilation_options_ignored() {
3417        let src = make_sources(&[("module Top {}", "a.veryl")]);
3418        let mut o1 = default_opts();
3419        let mut o2 = default_opts();
3420        // VCD path doesn't affect compilation
3421        o1.common.vcd = None;
3422        o2.common.vcd = Some("/tmp/dump.vcd".into());
3423        assert_eq!(
3424            build_cache_key(&src, "Top", &o1, None),
3425            build_cache_key(&src, "Top", &o2, None),
3426        );
3427    }
3428
3429    #[test]
3430    fn false_loops_differ() {
3431        let src = make_sources(&[("module Top {}", "a.veryl")]);
3432        let mut o1 = default_opts();
3433        let mut o2 = default_opts();
3434        o1.common.false_loops = vec![];
3435        o2.common.false_loops = vec![((vec![], vec!["a".into()]), (vec![], vec!["b".into()]))];
3436        assert_ne!(
3437            build_cache_key(&src, "Top", &o1, None),
3438            build_cache_key(&src, "Top", &o2, None),
3439        );
3440    }
3441
3442    #[test]
3443    fn true_loops_differ() {
3444        let src = make_sources(&[("module Top {}", "a.veryl")]);
3445        let mut o1 = default_opts();
3446        let mut o2 = default_opts();
3447        o1.common.true_loops = vec![];
3448        o2.common.true_loops = vec![((vec![], vec!["x".into()]), (vec![], vec!["y".into()]), 4)];
3449        assert_ne!(
3450            build_cache_key(&src, "Top", &o1, None),
3451            build_cache_key(&src, "Top", &o2, None),
3452        );
3453    }
3454
3455    #[test]
3456    fn metadata_clock_reset_differs() {
3457        let src = make_sources(&[("module Top {}", "a.veryl")]);
3458        let opts = default_opts();
3459
3460        let mut m1 = Metadata::create_default("prj").unwrap();
3461        let mut m2 = Metadata::create_default("prj").unwrap();
3462        m1.build.clock_type = celox::ClockType::PosEdge;
3463        m2.build.clock_type = celox::ClockType::NegEdge;
3464        assert_ne!(
3465            build_cache_key(&src, "Top", &opts, Some(&m1)),
3466            build_cache_key(&src, "Top", &opts, Some(&m2)),
3467        );
3468
3469        let mut m3 = Metadata::create_default("prj").unwrap();
3470        let mut m4 = Metadata::create_default("prj").unwrap();
3471        m3.build.reset_type = celox::ResetType::AsyncLow;
3472        m4.build.reset_type = celox::ResetType::SyncHigh;
3473        assert_ne!(
3474            build_cache_key(&src, "Top", &opts, Some(&m3)),
3475            build_cache_key(&src, "Top", &opts, Some(&m4)),
3476        );
3477    }
3478
3479    #[test]
3480    fn no_metadata_vs_metadata_differs() {
3481        let src = make_sources(&[("module Top {}", "a.veryl")]);
3482        let opts = default_opts();
3483        let m = Metadata::create_default("prj").unwrap();
3484        // No metadata vs with metadata should differ (metadata adds clock/reset info)
3485        assert_ne!(
3486            build_cache_key(&src, "Top", &opts, None),
3487            build_cache_key(&src, "Top", &opts, Some(&m)),
3488        );
3489    }
3490}