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    /// Simulation threads, including the calling thread. Default: 1.
114    pub threads: Option<u32>,
115}
116
117/// Parsed builder options from NapiOptions (common fields available on all targets).
118#[allow(dead_code)]
119struct ParsedOptionsCommon {
120    four_state: bool,
121    optimize_options: celox::OptimizeOptions,
122    vcd: Option<String>,
123    false_loops: Vec<(
124        (Vec<(String, usize)>, Vec<String>),
125        (Vec<(String, usize)>, Vec<String>),
126    )>,
127    true_loops: Vec<(
128        (Vec<(String, usize)>, Vec<String>),
129        (Vec<(String, usize)>, Vec<String>),
130        usize,
131    )>,
132    clock_type: Option<celox::ClockType>,
133    reset_type: Option<celox::ResetType>,
134    extra_source: Option<String>,
135    parameters: Vec<(String, u64)>,
136}
137
138/// Parsed builder options from NapiOptions (native-only, includes Cranelift/DSE options).
139#[cfg(not(target_arch = "wasm32"))]
140struct ParsedOptions {
141    common: ParsedOptionsCommon,
142    cranelift_options: celox::CraneliftOptions,
143    dead_store_policy: celox::DeadStorePolicy,
144}
145
146#[cfg(not(target_arch = "wasm32"))]
147impl std::ops::Deref for ParsedOptions {
148    type Target = ParsedOptionsCommon;
149    fn deref(&self) -> &Self::Target {
150        &self.common
151    }
152}
153
154/// Convert a NapiSignalPath to the Rust builder's tuple format.
155fn convert_signal_path(p: &NapiSignalPath) -> (Vec<(String, usize)>, Vec<String>) {
156    let inst: Vec<(String, usize)> = p
157        .instance_path
158        .iter()
159        .map(|seg| (seg.name.clone(), seg.index as usize))
160        .collect();
161    let var_path: Vec<String> = p.var_path.clone();
162    (inst, var_path)
163}
164
165/// Parse a clock type string into ClockType.
166fn parse_clock_type(s: &str) -> Result<celox::ClockType> {
167    match s {
168        "posedge" => Ok(celox::ClockType::PosEdge),
169        "negedge" => Ok(celox::ClockType::NegEdge),
170        _ => Err(Error::from_reason(format!(
171            "Invalid clock_type '{}'. Expected 'posedge' or 'negedge'.",
172            s
173        ))),
174    }
175}
176
177/// Parse a reset type string into ResetType.
178fn parse_reset_type(s: &str) -> Result<celox::ResetType> {
179    match s {
180        "async_high" => Ok(celox::ResetType::AsyncHigh),
181        "async_low" => Ok(celox::ResetType::AsyncLow),
182        "sync_high" => Ok(celox::ResetType::SyncHigh),
183        "sync_low" => Ok(celox::ResetType::SyncLow),
184        _ => Err(Error::from_reason(format!(
185            "Invalid reset_type '{}'. Expected 'async_high', 'async_low', 'sync_high', or 'sync_low'.",
186            s
187        ))),
188    }
189}
190
191/// Convert legacy NapiOptimizeOptions to celox::OptimizeOptions.
192/// Starts from O1 (all on) and disables any explicitly false fields.
193fn convert_optimize_options(napi: &NapiOptimizeOptions) -> celox::OptimizeOptions {
194    let mut opts = celox::OptimizeOptions::all();
195    let fields: &[(Option<bool>, celox::SirPass)] = &[
196        (
197            napi.store_load_forwarding,
198            celox::SirPass::StoreLoadForwarding,
199        ),
200        (
201            napi.hoist_common_branch_loads,
202            celox::SirPass::HoistCommonBranchLoads,
203        ),
204        (
205            napi.bit_extract_peephole,
206            celox::SirPass::BitExtractPeephole,
207        ),
208        (napi.optimize_blocks, celox::SirPass::OptimizeBlocks),
209        (napi.split_wide_commits, celox::SirPass::SplitWideCommits),
210        (napi.commit_sinking, celox::SirPass::CommitSinking),
211        (
212            napi.inline_commit_forwarding,
213            celox::SirPass::InlineCommitForwarding,
214        ),
215        (
216            napi.eliminate_dead_working_stores,
217            celox::SirPass::EliminateDeadWorkingStores,
218        ),
219        (napi.reschedule, celox::SirPass::Reschedule),
220        (napi.coalesce_stores, celox::SirPass::CoalesceStores),
221    ];
222    for &(val, pass) in fields {
223        if let Some(false) = val {
224            opts = opts.disable(pass);
225        }
226    }
227    opts
228}
229
230/// Parse pass override strings like "+sir:reschedule" or "-sir:coalesce_stores".
231fn apply_pass_overrides(
232    mut opts: celox::OptimizeOptions,
233    overrides: &[String],
234) -> Result<celox::OptimizeOptions> {
235    for s in overrides {
236        let (enable, rest) = if let Some(rest) = s.strip_prefix('+') {
237            (true, rest)
238        } else if let Some(rest) = s.strip_prefix('-') {
239            (false, rest)
240        } else {
241            return Err(Error::from_reason(format!(
242                "Invalid pass override '{}'. Must start with '+' or '-'.",
243                s
244            )));
245        };
246        let pass_name = rest.strip_prefix("sir:").unwrap_or(rest);
247        let pass = celox::SirPass::parse(pass_name).ok_or_else(|| {
248            Error::from_reason(format!(
249                "Unknown SIR pass '{}'. Valid passes: {}",
250                pass_name,
251                celox::SirPass::ALL
252                    .iter()
253                    .map(|p| p.as_str())
254                    .collect::<Vec<_>>()
255                    .join(", ")
256            ))
257        })?;
258        opts = if enable {
259            opts.enable(pass)
260        } else {
261            opts.disable(pass)
262        };
263    }
264    Ok(opts)
265}
266
267/// Parse a Cranelift optimization level string.
268#[cfg(not(target_arch = "wasm32"))]
269fn parse_cranelift_opt_level(s: &str) -> Result<celox::CraneliftOptLevel> {
270    match s {
271        "none" => Ok(celox::CraneliftOptLevel::None),
272        "speed" => Ok(celox::CraneliftOptLevel::Speed),
273        "speed_and_size" => Ok(celox::CraneliftOptLevel::SpeedAndSize),
274        _ => Err(Error::from_reason(format!(
275            "Invalid cranelift_opt_level '{}'. Expected 'none', 'speed', or 'speed_and_size'.",
276            s
277        ))),
278    }
279}
280
281/// Parse a register allocator algorithm string.
282#[cfg(not(target_arch = "wasm32"))]
283fn parse_regalloc_algorithm(s: &str) -> Result<celox::RegallocAlgorithm> {
284    match s {
285        "backtracking" => Ok(celox::RegallocAlgorithm::Backtracking),
286        "single_pass" => Ok(celox::RegallocAlgorithm::SinglePass),
287        _ => Err(Error::from_reason(format!(
288            "Invalid regalloc_algorithm '{}'. Expected 'backtracking' or 'single_pass'.",
289            s
290        ))),
291    }
292}
293
294/// Parse a dead store policy string into DeadStorePolicy.
295#[cfg(not(target_arch = "wasm32"))]
296fn parse_dead_store_policy(s: &str) -> Result<celox::DeadStorePolicy> {
297    match s {
298        "off" => Ok(celox::DeadStorePolicy::Off),
299        "preserve_top_ports" => Ok(celox::DeadStorePolicy::PreserveTopPorts),
300        "preserve_all_ports" => Ok(celox::DeadStorePolicy::PreserveAllPorts),
301        _ => Err(Error::from_reason(format!(
302            "Invalid dead_store_policy '{}'. Expected 'off', 'preserve_top_ports', or 'preserve_all_ports'.",
303            s
304        ))),
305    }
306}
307
308/// Helper to extract the common builder config from NapiOptions (available on all targets).
309fn parse_options_common(options: &Option<NapiOptions>) -> Result<ParsedOptionsCommon> {
310    match options.as_ref() {
311        Some(o) => {
312            let false_loops = o
313                .false_loops
314                .as_ref()
315                .map(|loops| {
316                    loops
317                        .iter()
318                        .map(|fl| (convert_signal_path(&fl.from), convert_signal_path(&fl.to)))
319                        .collect()
320                })
321                .unwrap_or_default();
322            let true_loops = o
323                .true_loops
324                .as_ref()
325                .map(|loops| {
326                    loops
327                        .iter()
328                        .map(|tl| {
329                            (
330                                convert_signal_path(&tl.from),
331                                convert_signal_path(&tl.to),
332                                tl.max_iter as usize,
333                            )
334                        })
335                        .collect()
336                })
337                .unwrap_or_default();
338            let clock_type = o.clock_type.as_deref().map(parse_clock_type).transpose()?;
339            let reset_type = o.reset_type.as_deref().map(parse_reset_type).transpose()?;
340            let parameters = o
341                .parameters
342                .as_ref()
343                .map(|params| {
344                    params
345                        .iter()
346                        .map(|p| (p.name.clone(), p.value as u64))
347                        .collect()
348                })
349                .unwrap_or_default();
350            // Resolve optimize_options with priority:
351            // 1. opt_level + pass_overrides (new API)
352            // 2. optimize_options (legacy per-pass bools)
353            // 3. optimize shorthand (legacy bool)
354            // 4. default (O1 = all on)
355            let optimize_options = if let Some(ref level_str) = o.opt_level {
356                let level = celox::OptLevel::parse(level_str).ok_or_else(|| {
357                    Error::from_reason(format!(
358                        "Invalid opt_level '{}'. Expected 'O0', 'O1', or 'O2'.",
359                        level_str
360                    ))
361                })?;
362                let opts = celox::OptimizeOptions::new(level);
363                if let Some(ref overrides) = o.pass_overrides {
364                    apply_pass_overrides(opts, overrides)?
365                } else {
366                    opts
367                }
368            } else if let Some(ref oo) = o.optimize_options {
369                let opts = convert_optimize_options(oo);
370                if let Some(ref overrides) = o.pass_overrides {
371                    apply_pass_overrides(opts, overrides)?
372                } else {
373                    opts
374                }
375            } else if let Some(false) = o.optimize {
376                celox::OptimizeOptions::none()
377            } else {
378                celox::OptimizeOptions::all()
379            };
380            Ok(ParsedOptionsCommon {
381                four_state: o.four_state.unwrap_or(false),
382                optimize_options,
383                vcd: o.vcd.clone(),
384                false_loops,
385                true_loops,
386                clock_type,
387                reset_type,
388                extra_source: o.extra_source.clone(),
389                parameters,
390            })
391        }
392        None => Ok(ParsedOptionsCommon {
393            four_state: false,
394            optimize_options: celox::OptimizeOptions::all(),
395            vcd: None,
396            false_loops: Vec::new(),
397            true_loops: Vec::new(),
398            clock_type: None,
399            reset_type: None,
400            extra_source: None,
401            parameters: Vec::new(),
402        }),
403    }
404}
405
406/// Helper to extract the full builder config from NapiOptions (native only).
407#[cfg(not(target_arch = "wasm32"))]
408fn parse_options(options: &Option<NapiOptions>) -> Result<ParsedOptions> {
409    let mut common = parse_options_common(options)?;
410    match options.as_ref() {
411        Some(o) => {
412            if let Some(threads) = o.threads {
413                if threads == 0 {
414                    return Err(Error::from_reason(
415                        "Invalid threads 0. Expected at least 1.".to_string(),
416                    ));
417                }
418                common
419                    .optimize_options
420                    .set_parallel_lanes(threads.min(celox::MAX_SIMULATION_THREADS));
421            }
422            let dead_store_policy = o
423                .dead_store_policy
424                .as_deref()
425                .map(parse_dead_store_policy)
426                .transpose()?
427                .unwrap_or(celox::DeadStorePolicy::Off);
428            let cranelift_opt_level = o
429                .cranelift_opt_level
430                .as_deref()
431                .map(parse_cranelift_opt_level)
432                .transpose()?
433                .unwrap_or(celox::CraneliftOptLevel::Speed);
434            let regalloc_algorithm = o
435                .regalloc_algorithm
436                .as_deref()
437                .map(parse_regalloc_algorithm)
438                .transpose()?
439                .unwrap_or(celox::RegallocAlgorithm::Backtracking);
440            let cranelift_options = celox::CraneliftOptions {
441                opt_level: cranelift_opt_level,
442                regalloc_algorithm,
443                enable_alias_analysis: o.enable_alias_analysis.unwrap_or(true),
444                enable_verifier: o.enable_verifier.unwrap_or(true),
445                tail_call_split: true,
446                diagnostics: celox::CraneliftDiagnostics::default(),
447            };
448            Ok(ParsedOptions {
449                common,
450                cranelift_options,
451                dead_store_policy,
452            })
453        }
454        None => Ok(ParsedOptions {
455            common,
456            cranelift_options: celox::CraneliftOptions::default(),
457            dead_store_policy: celox::DeadStorePolicy::Off,
458        }),
459    }
460}
461
462/// Append extra source as a separate file entry if provided.
463fn append_extra_source(sources: &mut Vec<(String, std::path::PathBuf)>, extra: &Option<String>) {
464    if let Some(extra) = extra {
465        sources.push((extra.clone(), std::path::PathBuf::from("<extra>")));
466    }
467}
468
469/// Configuration loaded from an optional `celox.toml` in the project root.
470#[derive(serde::Deserialize, Default)]
471#[allow(dead_code)]
472struct CeloxConfig {
473    /// Glob patterns (relative to project root) for `.veryl` files to exclude
474    /// from compilation and type generation.
475    #[serde(default)]
476    exclude: Vec<String>,
477    #[serde(default)]
478    test: CeloxTestConfig,
479    #[serde(default)]
480    simulation: CeloxSimulationConfig,
481}
482
483#[derive(serde::Deserialize, Default)]
484struct CeloxTestConfig {
485    /// Additional source directories (relative to `celox.toml`) whose `.veryl`
486    /// files are included when running simulations and generating type stubs.
487    #[serde(default)]
488    sources: Vec<String>,
489}
490
491#[derive(serde::Deserialize, Default)]
492#[allow(dead_code)]
493struct CeloxSimulationConfig {
494    /// Default maximum steps for `waitUntil` / `waitForCycles`.
495    /// Overridden by the per-call `maxSteps` option.
496    max_steps: Option<u32>,
497}
498
499/// Load `celox.toml` from the given project root (same directory as `Veryl.toml`).
500/// Returns `None` if the file does not exist.
501fn load_celox_config(project_root: &std::path::Path) -> Result<CeloxConfig> {
502    let path = project_root.join("celox.toml");
503    if !path.exists() {
504        return Ok(CeloxConfig::default());
505    }
506    let content = std::fs::read_to_string(&path)
507        .map_err(|e| Error::from_reason(format!("Failed to read celox.toml: {e}")))?;
508    toml::from_str(&content)
509        .map_err(|e| Error::from_reason(format!("Failed to parse celox.toml: {e}")))
510}
511
512/// Build a `GlobSet` from the exclude patterns in the config.
513/// Returns `None` if there are no exclude patterns.
514fn build_exclude_set(config: &CeloxConfig) -> Result<Option<globset::GlobSet>> {
515    if config.exclude.is_empty() {
516        return Ok(None);
517    }
518    let mut builder = globset::GlobSetBuilder::new();
519    for pattern in &config.exclude {
520        let glob = globset::GlobBuilder::new(pattern)
521            .literal_separator(true)
522            .build()
523            .map_err(|e| Error::from_reason(format!("Invalid exclude pattern '{pattern}': {e}")))?;
524        builder.add(glob);
525    }
526    let set = builder
527        .build()
528        .map_err(|e| Error::from_reason(format!("Failed to build exclude set: {e}")))?;
529    Ok(Some(set))
530}
531
532/// Returns `true` if the path should be excluded based on the glob set.
533/// The path is matched relative to `project_root`.
534fn is_excluded(
535    path: &std::path::Path,
536    project_root: &std::path::Path,
537    exclude_set: &globset::GlobSet,
538) -> bool {
539    let relative = path.strip_prefix(project_root).unwrap_or(path);
540    // Normalize to forward slashes for consistent matching
541    let rel_str = relative.to_string_lossy().replace('\\', "/");
542    exclude_set.is_match(&rel_str)
543}
544
545/// Collect all `.veryl` files from the extra test source directories declared in
546/// `celox.toml` and add them as individual source entries.
547fn collect_test_sources(
548    sources: &mut Vec<(String, std::path::PathBuf)>,
549    project_root: &std::path::Path,
550    config: &CeloxConfig,
551) -> Result<()> {
552    for dir in &config.test.sources {
553        let dir_path = project_root.join(dir);
554        if !dir_path.exists() {
555            continue;
556        }
557        let entries = walkdir(&dir_path)?;
558        for entry in entries {
559            let content = std::fs::read_to_string(&entry)
560                .map_err(|e| Error::from_reason(format!("{}: {e}", entry.display())))?;
561            sources.push((content, entry));
562        }
563    }
564    Ok(())
565}
566
567/// Recursively collect `.veryl` files under `dir`, sorted for determinism.
568fn walkdir(dir: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
569    let mut files = Vec::new();
570    let read = std::fs::read_dir(dir)
571        .map_err(|e| Error::from_reason(format!("Cannot read directory {}: {e}", dir.display())))?;
572    for entry in read {
573        let entry = entry.map_err(|e| Error::from_reason(format!("Directory entry error: {e}")))?;
574        let path = entry.path();
575        if path.is_dir() {
576            files.extend(walkdir(&path)?);
577        } else if path.extension().is_some_and(|ext| ext == "veryl") {
578            files.push(path);
579        }
580    }
581    files.sort();
582    Ok(files)
583}
584
585/// Load a Veryl project's source files and metadata from a directory.
586///
587/// Searches upward from `project_path` for `Veryl.toml`, gathers all `.veryl`
588/// source files, and returns the per-file sources, project metadata, and
589/// the parsed `celox.toml` configuration.
590fn load_project_sources(
591    project_path: &str,
592) -> Result<(Vec<(String, std::path::PathBuf)>, Metadata, CeloxConfig)> {
593    let toml_path = Metadata::search_from(project_path)
594        .map_err(|e| Error::from_reason(format!("Could not find Veryl.toml: {e}")))?;
595    let mut metadata = Metadata::load(&toml_path)
596        .map_err(|e| Error::from_reason(format!("Failed to load Veryl.toml: {e}")))?;
597    let paths = metadata
598        .paths::<&str>(&[], false, false)
599        .map_err(|e| Error::from_reason(format!("Failed to gather sources: {e}")))?;
600    let mut sources = Vec::new();
601    for p in paths.iter().filter(|path| !path.example) {
602        let content = std::fs::read_to_string(&p.src)
603            .map_err(|e| Error::from_reason(format!("{}: {e}", p.src.display())))?;
604        sources.push((content, p.src.clone()));
605    }
606    let project_root = toml_path.parent().unwrap_or(&toml_path);
607    let celox_cfg = load_celox_config(project_root)?;
608    collect_test_sources(&mut sources, project_root, &celox_cfg)?;
609    if let Some(exclude_set) = build_exclude_set(&celox_cfg)? {
610        sources.retain(|(_, path)| !is_excluded(path, project_root, &exclude_set));
611    }
612    Ok((sources, metadata, celox_cfg))
613}
614
615/// Format compilation warnings as a JSON array of strings.
616///
617/// Uses `render_diagnostic` to include source location and span information,
618/// matching the format used for error messages.
619fn format_warnings_json(warnings: &[celox::CompilationWarning]) -> String {
620    let msgs: Vec<String> = warnings
621        .iter()
622        .map(|w| celox::render_diagnostic(w))
623        .collect();
624    serde_json::to_string(&msgs).unwrap_or_else(|_| "[]".to_string())
625}
626
627/// Apply parsed options to a SimulatorBuilder.
628#[cfg(not(target_arch = "wasm32"))]
629fn apply_options<'a, T>(
630    mut builder: celox::SimulatorBuilder<'a, T>,
631    opts: &ParsedOptions,
632) -> celox::SimulatorBuilder<'a, T> {
633    builder = builder.four_state(opts.four_state);
634    builder = builder.optimize_options(opts.optimize_options.clone());
635    builder = builder.threads(opts.optimize_options.parallel_lanes() as usize);
636    builder = builder.cranelift_options(opts.cranelift_options);
637    // VCD changes the layout and generated stores, so enable it before building.
638    if let Some(path) = opts.vcd.as_deref() {
639        builder = builder.vcd(path);
640    }
641    for (from, to) in &opts.false_loops {
642        builder = builder.false_loop(from.clone(), to.clone());
643    }
644    for (from, to, max_iter) in &opts.true_loops {
645        builder = builder.true_loop(from.clone(), to.clone(), *max_iter);
646    }
647    if let Some(ct) = opts.clock_type {
648        builder = builder.clock_type(ct);
649    }
650    if let Some(rt) = opts.reset_type {
651        builder = builder.reset_type(rt);
652    }
653    for (name, value) in &opts.parameters {
654        builder = builder.param(name, *value);
655    }
656    builder = builder.dead_store_policy(opts.dead_store_policy);
657    builder
658}
659
660// ---------------------------------------------------------------------------
661//  Process-global compiled-code cache (native only)
662// ---------------------------------------------------------------------------
663
664#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
665type SharedCode = celox::SharedNativeCode;
666#[cfg(all(
667    not(target_arch = "wasm32"),
668    not(any(target_arch = "x86_64", target_arch = "aarch64"))
669))]
670type SharedCode = celox::SharedJitCode;
671
672#[cfg(not(target_arch = "wasm32"))]
673/// Cached compilation result shared across simulator instances.
674struct CachedBuild {
675    shared_code: Arc<SharedCode>,
676    runtime_errors: HashMap<i64, (String, Vec<String>)>,
677    layout_json: String,
678    events_json: String,
679    hierarchy_json: String,
680    warnings_json: String,
681    stable_size: u32,
682    total_size: u32,
683    state_fingerprint: u64,
684    state_schema: Arc<celox::StateSchema>,
685    /// Pre-computed VCD signal descriptors so VCD works on cache hits.
686    vcd_descs: Vec<celox::VcdSignalDesc>,
687}
688
689#[cfg(not(target_arch = "wasm32"))]
690/// Exact cache key — no hashing, no collisions.
691///
692/// Contains the full source content + paths + top module + all compilation-
693/// affecting options. Two builds produce the same `CacheKey` iff they would
694/// produce identical compiled code.
695#[derive(Clone, Debug, PartialEq, Eq, Hash)]
696struct CacheKey {
697    /// (path, content) sorted by path for determinism.
698    sources: Vec<(String, String)>,
699    top: String,
700    four_state: bool,
701    vcd_tracking: bool,
702    sir_optimization: SirOptimizationCacheKey,
703    cranelift_opt_level: u8,
704    regalloc_algorithm: u8,
705    enable_alias_analysis: bool,
706    enable_verifier: bool,
707    dead_store_policy: u8,
708    clock_type: Option<u8>,
709    reset_type: Option<u8>,
710    parameters: Vec<(String, u64)>,
711    false_loops: Vec<(
712        (Vec<(String, usize)>, Vec<String>),
713        (Vec<(String, usize)>, Vec<String>),
714    )>,
715    true_loops: Vec<(
716        (Vec<(String, usize)>, Vec<String>),
717        (Vec<(String, usize)>, Vec<String>),
718        usize,
719    )>,
720    /// Effective clock/reset from metadata (from_project path).
721    /// None when using the `new` constructor (no metadata).
722    metadata_clock_type: Option<u8>,
723    metadata_reset_type: Option<u8>,
724}
725
726/// Collision-free representation of every SIR code-generation option.
727#[cfg(not(target_arch = "wasm32"))]
728#[derive(Clone, Debug, PartialEq, Eq, Hash)]
729struct SirOptimizationCacheKey {
730    opt_level: celox::OptLevel,
731    enabled_passes: Box<[bool]>,
732    max_native_memory_width: usize,
733    parallel_lanes: u32,
734    always_partition: bool,
735}
736
737#[cfg(not(target_arch = "wasm32"))]
738impl From<&celox::OptimizeOptions> for SirOptimizationCacheKey {
739    fn from(options: &celox::OptimizeOptions) -> Self {
740        Self {
741            opt_level: options.opt_level(),
742            enabled_passes: celox::SirPass::ALL
743                .iter()
744                .map(|&pass| options.is_enabled(pass))
745                .collect(),
746            max_native_memory_width: options.max_native_memory_width(),
747            parallel_lanes: options.parallel_lanes(),
748            always_partition: options.parallel_partition() == celox::ParallelPartition::Always,
749        }
750    }
751}
752
753#[cfg(not(target_arch = "wasm32"))]
754static JIT_CACHE: std::sync::LazyLock<Mutex<HashMap<CacheKey, Arc<CachedBuild>>>> =
755    std::sync::LazyLock::new(|| Mutex::new(HashMap::default()));
756
757#[cfg(not(target_arch = "wasm32"))]
758/// Build a collision-free cache key from source content, top module, and options.
759///
760/// When `metadata` is `Some`, the effective clock/reset settings from
761/// `Veryl.toml` are included in the key so that changing project config
762/// invalidates the cache.
763fn build_cache_key(
764    sources: &[(String, std::path::PathBuf)],
765    top: &str,
766    opts: &ParsedOptions,
767    metadata: Option<&Metadata>,
768) -> CacheKey {
769    let mut sorted_sources: Vec<(String, String)> = sources
770        .iter()
771        .map(|(content, path)| (path.to_string_lossy().into_owned(), content.clone()))
772        .collect();
773    sorted_sources.sort_by(|a, b| a.0.cmp(&b.0));
774
775    CacheKey {
776        sources: sorted_sources,
777        top: top.to_string(),
778        four_state: opts.four_state,
779        vcd_tracking: opts.vcd.is_some(),
780        sir_optimization: SirOptimizationCacheKey::from(&opts.optimize_options),
781        cranelift_opt_level: opts.cranelift_options.opt_level as u8,
782        regalloc_algorithm: opts.cranelift_options.regalloc_algorithm as u8,
783        enable_alias_analysis: opts.cranelift_options.enable_alias_analysis,
784        enable_verifier: opts.cranelift_options.enable_verifier,
785        dead_store_policy: opts.dead_store_policy as u8,
786        clock_type: opts.clock_type.map(|ct| ct as u8),
787        reset_type: opts.reset_type.map(|rt| rt as u8),
788        parameters: opts.parameters.clone(),
789        false_loops: opts.false_loops.clone(),
790        true_loops: opts.true_loops.clone(),
791        metadata_clock_type: metadata.map(|m| m.build.clock_type as u8),
792        metadata_reset_type: metadata.map(|m| m.build.reset_type as u8),
793    }
794}
795
796#[cfg(not(target_arch = "wasm32"))]
797fn runtime_errors_by_name(program: &celox::RuntimeProgram) -> HashMap<i64, (String, Vec<String>)> {
798    program
799        .runtime_schema
800        .runtime_errors
801        .iter()
802        .map(|(&code, info)| {
803            (
804                code,
805                (
806                    info.message.clone(),
807                    info.signals
808                        .iter()
809                        .map(|addr| program.get_path(addr))
810                        .collect(),
811                ),
812            )
813        })
814        .collect()
815}
816
817#[cfg(not(target_arch = "wasm32"))]
818fn napi_runtime_error(
819    runtime_errors: &HashMap<i64, (String, Vec<String>)>,
820    err: celox::RuntimeErrorCode,
821) -> Error {
822    match err {
823        celox::RuntimeErrorCode::DetectedTrueLoopCode(code) => {
824            if let Some((message, signals)) = runtime_errors.get(&code) {
825                if message == "Detected True Loop" {
826                    Error::from_reason(format!(
827                        "{}",
828                        celox::RuntimeErrorCode::DetectedTrueLoopAt {
829                            signals: signals.clone(),
830                        }
831                    ))
832                } else {
833                    Error::from_reason(format!(
834                        "{}",
835                        celox::RuntimeErrorCode::Runtime {
836                            message: message.clone(),
837                            signals: signals.clone(),
838                        }
839                    ))
840                }
841            } else {
842                Error::from_reason(format!("{}", celox::RuntimeErrorCode::DetectedTrueLoop))
843            }
844        }
845        other => Error::from_reason(format!("{}", other)),
846    }
847}
848
849/// Validate a raw numeric event ID before it reaches an unchecked backend lookup.
850///
851/// Standard Celox metadata assigns IDs by enumerating the backend event table, so
852/// valid IDs are dense in `0..event_count`. Numeric N-API entry points must keep
853/// this check in front of every backend call that indexes by event ID.
854#[cfg(not(target_arch = "wasm32"))]
855fn validate_event_id(event_id: u32, event_count: usize) -> Result<()> {
856    if (event_id as usize) < event_count {
857        return Ok(());
858    }
859
860    Err(Error::from_reason(
861        celox::RuntimeErrorCode::NotAnEvent(format!("event_id={event_id}")).to_string(),
862    ))
863}
864
865/// The backend driving a [`NativeSimulatorHandle`].
866///
867/// Either the default compiled backend (as before) or the tiered backend,
868/// which starts on the interpreter and promotes to generated code in the
869/// background. Dispatch goes through the shared [`SimBackend`] surface the
870/// handle actually uses.
871#[cfg(not(target_arch = "wasm32"))]
872enum HandleBackend {
873    Default(celox::DefaultBackend),
874    Tiered(Box<celox::TieredBackend>),
875}
876
877#[cfg(not(target_arch = "wasm32"))]
878impl HandleBackend {
879    fn event_count(&self) -> usize {
880        match self {
881            Self::Default(backend) => backend.id_to_event_slice().len(),
882            Self::Tiered(backend) => backend.id_to_event_slice().len(),
883        }
884    }
885
886    fn eval_comb(&mut self) -> std::result::Result<(), celox::RuntimeErrorCode> {
887        match self {
888            Self::Default(backend) => backend.eval_comb(),
889            Self::Tiered(backend) => backend.eval_comb(),
890        }
891    }
892
893    fn eval_apply_ff_at(
894        &mut self,
895        event_id: usize,
896    ) -> std::result::Result<(), celox::RuntimeErrorCode> {
897        match self {
898            Self::Default(backend) => {
899                let event = backend.id_to_event_slice()[event_id];
900                backend.eval_apply_ff_at(event)
901            }
902            Self::Tiered(backend) => {
903                let event = backend.id_to_event_slice()[event_id];
904                backend.eval_apply_ff_at(event)
905            }
906        }
907    }
908
909    fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
910        match self {
911            Self::Default(backend) => backend.memory_as_mut_ptr(),
912            Self::Tiered(backend) => backend.memory_as_mut_ptr(),
913        }
914    }
915
916    fn memory_owner(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
917        match self {
918            Self::Default(backend) => backend.memory_owner(),
919            Self::Tiered(backend) => backend.memory_owner(),
920        }
921    }
922
923    fn stable_region_size(&self) -> usize {
924        match self {
925            Self::Default(backend) => backend.stable_region_size(),
926            Self::Tiered(backend) => backend.stable_region_size(),
927        }
928    }
929
930    fn capture_state(&self, fingerprint: u64) -> celox::StateImage {
931        match self {
932            Self::Default(backend) => celox::StateImage::capture(backend, fingerprint),
933            Self::Tiered(backend) => celox::StateImage::capture(backend.as_ref(), fingerprint),
934        }
935    }
936
937    fn save_state_file(&self, schema: &celox::StateSchema) -> celox::StateFile {
938        match self {
939            Self::Default(backend) => schema.capture(backend),
940            Self::Tiered(backend) => schema.capture(backend.as_ref()),
941        }
942    }
943
944    fn load_state_file(
945        &mut self,
946        schema: &celox::StateSchema,
947        file: &celox::StateFile,
948    ) -> std::result::Result<(), celox::StateMismatch> {
949        match self {
950            Self::Default(backend) => schema.load(backend, file),
951            Self::Tiered(backend) => schema.load(backend.as_mut(), file),
952        }
953    }
954
955    fn restore_state(
956        &mut self,
957        image: &celox::StateImage,
958        fingerprint: u64,
959    ) -> std::result::Result<(), celox::CheckpointError> {
960        match self {
961            Self::Default(backend) => image.restore_into(backend, fingerprint),
962            Self::Tiered(backend) => image.restore_into(backend.as_mut(), fingerprint),
963        }
964    }
965}
966
967/// Low-level handle wrapping the default backend and optional VCD writer.
968///
969/// JS holds this as an opaque class; all operations go through methods.
970#[cfg(not(target_arch = "wasm32"))]
971#[napi]
972pub struct NativeSimulatorHandle {
973    backend: Option<HandleBackend>,
974    runtime_errors: HashMap<i64, (String, Vec<String>)>,
975    vcd_writer: Option<celox::VcdWriter>,
976    layout_json: String,
977    events_json: String,
978    hierarchy_json: String,
979    warnings_json: String,
980    stable_size: u32,
981    total_size: u32,
982    state_fingerprint: u64,
983    state_schema: Arc<celox::StateSchema>,
984}
985
986#[cfg(not(target_arch = "wasm32"))]
987impl NativeSimulatorHandle {
988    /// Wrap a simulator built by an external frontend in the standard Celox
989    /// N-API handle.
990    ///
991    /// This is a Rust-only adapter API. A frontend binding can accept its own
992    /// artifact type in a `#[napi]` function, lower it with
993    /// `celox-frontend-sdk`, build a [`celox::Simulator`], and pass the value
994    /// here without serializing an artifact to JSON. Signal metadata is always
995    /// derived from the simulator's actual memory layout.
996    pub fn from_simulator(simulator: celox::Simulator, vcd_path: Option<&str>) -> Result<Self> {
997        Self::build_and_cache(simulator, vcd_path, None)
998    }
999
1000    /// Build an N-API handle directly from an in-memory frontend artifact.
1001    ///
1002    /// Frontend bindings normally expose an artifact-specific function such
1003    /// as `from_my_artifact` and use this as their final adapter step.
1004    pub fn from_frontend(
1005        artifact: celox::FrontendArtifact,
1006        options: Option<NapiOptions>,
1007    ) -> Result<Self> {
1008        let opts = parse_options(&options)?;
1009        let builder = apply_options(celox::Simulator::from_frontend(artifact), &opts);
1010        let simulator = builder
1011            .build()
1012            .map_err(|error| Error::from_reason(error.to_string()))?;
1013        Self::from_simulator(simulator, opts.vcd.as_deref())
1014    }
1015}
1016
1017#[cfg(not(target_arch = "wasm32"))]
1018#[napi]
1019impl NativeSimulatorHandle {
1020    /// Build a full simulator, extract metadata, cache the compiled code,
1021    /// and return the handle with the default backend (and optional VcdWriter).
1022    fn build_and_cache(
1023        sim: celox::Simulator,
1024        vcd_path: Option<&str>,
1025        cache_key: Option<CacheKey>,
1026    ) -> Result<Self> {
1027        let four_state = sim.layout().four_state;
1028        let warnings_json = format_warnings_json(sim.warnings());
1029        let signals = sim.named_signals();
1030        let events = sim.named_events();
1031        let hierarchy = sim.named_hierarchy();
1032        let (_, total_size) = sim.memory_as_ptr();
1033        let stable_size = sim.stable_region_size();
1034        let state_fingerprint = sim.state_fingerprint();
1035        let state_schema = sim.state_schema();
1036        let vcd_descs = sim.build_vcd_descs(four_state);
1037        let runtime_errors = runtime_errors_by_name(sim.program());
1038
1039        let layout_map = build_signal_layout(&signals, four_state);
1040        let event_map = build_event_map(&events);
1041        let hierarchy_node = build_hierarchy_node(&hierarchy, four_state);
1042
1043        let layout_json = serde_json::to_string(&layout_map)
1044            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1045        let events_json = serde_json::to_string(&event_map)
1046            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1047        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1048            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1049
1050        // Cache the compiled code + metadata for future instances
1051        if let Some(key) = cache_key {
1052            let cached = Arc::new(CachedBuild {
1053                shared_code: sim.shared_code(),
1054                runtime_errors: runtime_errors.clone(),
1055                layout_json: layout_json.clone(),
1056                events_json: events_json.clone(),
1057                hierarchy_json: hierarchy_json.clone(),
1058                warnings_json: warnings_json.clone(),
1059                stable_size: stable_size as u32,
1060                total_size: total_size as u32,
1061                state_fingerprint,
1062                state_schema: Arc::clone(&state_schema),
1063                vcd_descs: vcd_descs.clone(),
1064            });
1065            let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1066            cache.insert(key, cached);
1067        }
1068
1069        // Create VcdWriter if requested
1070        let vcd_writer = if let Some(path) = vcd_path {
1071            Some(
1072                celox::VcdWriter::new(path, &vcd_descs)
1073                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
1074            )
1075        } else {
1076            None
1077        };
1078
1079        // Extract the backend from Simulator (drops runtime metadata which is no longer needed)
1080        let backend = HandleBackend::Default(sim.into_backend());
1081
1082        Ok(Self {
1083            backend: Some(backend),
1084            runtime_errors,
1085            vcd_writer,
1086            layout_json,
1087            events_json,
1088            hierarchy_json,
1089            warnings_json,
1090            stable_size: stable_size as u32,
1091            total_size: total_size as u32,
1092            state_fingerprint,
1093            state_schema,
1094        })
1095    }
1096
1097    /// Extract metadata from a tiered simulator and wrap its backend.
1098    ///
1099    /// Mirrors [`Self::build_and_cache`] minus the shared-code cache: the
1100    /// tiered backend owns its interpreter state and compiles in the
1101    /// background, so there is no compiled artifact to reuse yet.
1102    fn build_and_cache_tiered(mut sim: celox::Simulator<celox::TieredBackend>) -> Result<Self> {
1103        // The builder creates the VCD writer itself when recording was
1104        // requested, selecting a VCD-compatible packed layout in the process;
1105        // reuse that writer instead of rebuilding descriptors here.
1106        let vcd_writer = sim.take_vcd_writer();
1107        let four_state = sim.layout().four_state;
1108        let warnings_json = format_warnings_json(sim.warnings());
1109        let signals = sim.named_signals();
1110        let events = sim.named_events();
1111        let hierarchy = sim.named_hierarchy();
1112        let (_, total_size) = sim.memory_as_ptr();
1113        let stable_size = sim.stable_region_size();
1114        let state_fingerprint = sim.state_fingerprint();
1115        let state_schema = sim.state_schema();
1116        let runtime_errors = runtime_errors_by_name(sim.program());
1117
1118        let layout_map = build_signal_layout(&signals, four_state);
1119        let event_map = build_event_map(&events);
1120        let hierarchy_node = build_hierarchy_node(&hierarchy, four_state);
1121
1122        let layout_json = serde_json::to_string(&layout_map)
1123            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1124        let events_json = serde_json::to_string(&event_map)
1125            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1126        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1127            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1128
1129        // Extract the backend from Simulator (drops runtime metadata which is
1130        // no longer needed).
1131        let backend = HandleBackend::Tiered(Box::new(sim.into_backend()));
1132
1133        Ok(Self {
1134            backend: Some(backend),
1135            runtime_errors,
1136            vcd_writer,
1137            layout_json,
1138            events_json,
1139            hierarchy_json,
1140            warnings_json,
1141            stable_size: stable_size as u32,
1142            total_size: total_size as u32,
1143            state_fingerprint,
1144            state_schema,
1145        })
1146    }
1147
1148    /// Create a handle from a cached build (shared compiled code + fresh memory).
1149    fn from_cached(cached: &CachedBuild, vcd_path: Option<&str>) -> Result<Self> {
1150        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1151        let backend = HandleBackend::Default(celox::NativeBackend::from_shared(Arc::clone(
1152            &cached.shared_code,
1153        )));
1154        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1155        let backend = HandleBackend::Default(celox::JitBackend::from_shared(Arc::clone(
1156            &cached.shared_code,
1157        )));
1158        let vcd_writer = if let Some(path) = vcd_path {
1159            Some(
1160                celox::VcdWriter::new(path, &cached.vcd_descs)
1161                    .map_err(|e| Error::from_reason(format!("Failed to create VCD: {}", e)))?,
1162            )
1163        } else {
1164            None
1165        };
1166        Ok(Self {
1167            backend: Some(backend),
1168            runtime_errors: cached.runtime_errors.clone(),
1169            vcd_writer,
1170            layout_json: cached.layout_json.clone(),
1171            events_json: cached.events_json.clone(),
1172            hierarchy_json: cached.hierarchy_json.clone(),
1173            warnings_json: cached.warnings_json.clone(),
1174            stable_size: cached.stable_size,
1175            total_size: cached.total_size,
1176            state_fingerprint: cached.state_fingerprint,
1177            state_schema: Arc::clone(&cached.state_schema),
1178        })
1179    }
1180
1181    /// Create a new simulator from Veryl source code.
1182    #[napi(constructor)]
1183    pub fn new(
1184        sources: Vec<NapiSourceFile>,
1185        top: String,
1186        options: Option<NapiOptions>,
1187    ) -> Result<Self> {
1188        let opts = parse_options(&options)?;
1189        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1190            .into_iter()
1191            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1192            .collect();
1193        append_extra_source(&mut src_pairs, &opts.extra_source);
1194
1195        let cache_key = build_cache_key(&src_pairs, &top, &opts, None);
1196
1197        {
1198            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1199            if let Some(cached) = cache.get(&cache_key) {
1200                return Self::from_cached(cached, opts.vcd.as_deref());
1201            }
1202        }
1203
1204        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1205            .iter()
1206            .map(|(s, p)| (s.as_str(), p.as_path()))
1207            .collect();
1208        let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts);
1209        let sim = builder
1210            .build()
1211            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1212
1213        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1214    }
1215
1216    /// Create a new tiered simulator from Veryl source code.
1217    ///
1218    /// Execution starts on the interpreter immediately while the host's
1219    /// default compiled tier (native where available) is prepared on a
1220    /// background thread. The first safe point after compilation completes
1221    /// adopts the compiled code; observe progress through
1222    /// [`Self::tier_compiled`](Self::tier_compiled). Tiered builds bypass the
1223    /// JIT cache because the interpreter already removes startup latency.
1224    #[napi(factory)]
1225    pub fn new_tiered(
1226        sources: Vec<NapiSourceFile>,
1227        top: String,
1228        options: Option<NapiOptions>,
1229    ) -> Result<Self> {
1230        let opts = parse_options(&options)?;
1231        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1232            .into_iter()
1233            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1234            .collect();
1235        append_extra_source(&mut src_pairs, &opts.extra_source);
1236
1237        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1238            .iter()
1239            .map(|(s, p)| (s.as_str(), p.as_path()))
1240            .collect();
1241        let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts);
1242        let sim = builder
1243            .build_tiered()
1244            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1245
1246        Self::build_and_cache_tiered(sim)
1247    }
1248
1249    /// Create a new tiered simulator from a Veryl project directory.
1250    ///
1251    /// Tiered counterpart of [`Self::from_project`]: searches upward from
1252    /// `project_path` for `Veryl.toml`, starts on the interpreter
1253    /// immediately, and promotes to the host's default compiled tier in the
1254    /// background. Bypasses the JIT cache for the same reason as
1255    /// [`Self::new_tiered`].
1256    #[napi(factory)]
1257    pub fn new_tiered_from_project(
1258        project_path: String,
1259        top: String,
1260        options: Option<NapiOptions>,
1261    ) -> Result<Self> {
1262        let opts = parse_options(&options)?;
1263        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1264        append_extra_source(&mut sources, &opts.extra_source);
1265
1266        let source_refs: Vec<(&str, &std::path::Path)> = sources
1267            .iter()
1268            .map(|(s, p)| (s.as_str(), p.as_path()))
1269            .collect();
1270
1271        let builder = apply_options(
1272            celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
1273            &opts,
1274        );
1275        let sim = builder
1276            .build_tiered()
1277            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1278
1279        Self::build_and_cache_tiered(sim)
1280    }
1281
1282    /// Create a simulator from a versioned external-frontend artifact.
1283    #[napi(factory)]
1284    pub fn from_frontend_artifact(
1285        artifact_json: String,
1286        options: Option<NapiOptions>,
1287    ) -> Result<Self> {
1288        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1289            .map_err(|error| Error::from_reason(error.to_string()))?;
1290        Self::from_frontend(artifact, options)
1291    }
1292
1293    /// Create a new simulator from a Veryl project directory.
1294    ///
1295    /// Searches upward from `project_path` for `Veryl.toml`, gathers all
1296    /// `.veryl` source files, and builds the simulator using the project's
1297    /// clock/reset settings.
1298    #[napi(factory)]
1299    pub fn from_project(
1300        project_path: String,
1301        top: String,
1302        options: Option<NapiOptions>,
1303    ) -> Result<Self> {
1304        let opts = parse_options(&options)?;
1305        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
1306        append_extra_source(&mut sources, &opts.extra_source);
1307
1308        let cache_key = build_cache_key(&sources, &top, &opts, Some(&metadata));
1309
1310        {
1311            let cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
1312            if let Some(cached) = cache.get(&cache_key) {
1313                return Self::from_cached(cached, opts.vcd.as_deref());
1314            }
1315        }
1316
1317        let source_refs: Vec<(&str, &std::path::Path)> = sources
1318            .iter()
1319            .map(|(s, p)| (s.as_str(), p.as_path()))
1320            .collect();
1321
1322        let builder = apply_options(
1323            celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
1324            &opts,
1325        );
1326        let sim = builder
1327            .build()
1328            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1329
1330        Self::build_and_cache(sim, opts.vcd.as_deref(), Some(cache_key))
1331    }
1332
1333    /// Returns the signal layout as a JSON string.
1334    #[napi(getter)]
1335    pub fn layout_json(&self) -> String {
1336        self.layout_json.clone()
1337    }
1338
1339    /// Returns the event map as a JSON string.
1340    #[napi(getter)]
1341    pub fn events_json(&self) -> String {
1342        self.events_json.clone()
1343    }
1344
1345    /// Returns the instance hierarchy as a JSON string.
1346    #[napi(getter)]
1347    pub fn hierarchy_json(&self) -> String {
1348        self.hierarchy_json.clone()
1349    }
1350
1351    /// Returns compilation warnings as a JSON array of strings.
1352    #[napi(getter)]
1353    pub fn warnings_json(&self) -> String {
1354        self.warnings_json.clone()
1355    }
1356
1357    /// Returns the stable region size in bytes.
1358    #[napi(getter)]
1359    pub fn stable_size(&self) -> u32 {
1360        self.stable_size
1361    }
1362
1363    /// Returns the total memory size in bytes.
1364    #[napi(getter)]
1365    pub fn total_size(&self) -> u32 {
1366        self.total_size
1367    }
1368
1369    /// Whether this handle drives the tiered backend.
1370    #[napi(getter)]
1371    pub fn is_tiered(&self) -> bool {
1372        matches!(self.backend, Some(HandleBackend::Tiered(_)))
1373    }
1374
1375    /// Whether the tiered backend has adopted its compiled tier.
1376    ///
1377    /// `None` when the handle is not tiered; `false` while background
1378    /// compilation is still running or after it failed (the interpreter then
1379    /// remains the permanent tier).
1380    #[napi(getter)]
1381    pub fn tier_compiled(&self) -> Option<bool> {
1382        match &self.backend {
1383            Some(HandleBackend::Tiered(backend)) => Some(backend.is_compiled()),
1384            _ => None,
1385        }
1386    }
1387
1388    /// Trigger a clock/event by its numeric ID.
1389    #[napi]
1390    pub fn tick(&mut self, event_id: u32) -> Result<()> {
1391        let runtime_errors = self.runtime_errors.clone();
1392        let b = self
1393            .backend
1394            .as_mut()
1395            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1396        validate_event_id(event_id, b.event_count())?;
1397        b.eval_comb()
1398            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1399        b.eval_apply_ff_at(event_id as usize)
1400            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1401        b.eval_comb()
1402            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1403    }
1404
1405    /// Trigger a clock/event N times in a single NAPI call.
1406    #[napi]
1407    pub fn tick_n(&mut self, event_id: u32, count: u32) -> Result<()> {
1408        let runtime_errors = self.runtime_errors.clone();
1409        let b = self
1410            .backend
1411            .as_mut()
1412            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1413        // Validate even when `count` is zero so invalid raw IDs never receive
1414        // call-shape-dependent treatment at the N-API boundary.
1415        validate_event_id(event_id, b.event_count())?;
1416        for _ in 0..count {
1417            b.eval_comb()
1418                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1419            b.eval_apply_ff_at(event_id as usize)
1420                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1421            b.eval_comb()
1422                .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1423        }
1424        Ok(())
1425    }
1426
1427    /// Evaluate combinational logic.
1428    #[napi]
1429    pub fn eval_comb(&mut self) -> Result<()> {
1430        let runtime_errors = self.runtime_errors.clone();
1431        let b = self
1432            .backend
1433            .as_mut()
1434            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1435        b.eval_comb()
1436            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1437    }
1438
1439    /// Write VCD dump at the given timestamp.
1440    #[napi]
1441    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1442        let b = self
1443            .backend
1444            .as_mut()
1445            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1446        if let Some(ref mut writer) = self.vcd_writer {
1447            match b {
1448                HandleBackend::Default(backend) => {
1449                    writer.dump_backend(timestamp as u64, backend, &[])
1450                }
1451                HandleBackend::Tiered(backend) => {
1452                    writer.dump_backend(timestamp as u64, backend.as_mut(), &[])
1453                }
1454            }
1455            .map_err(|e| Error::from_reason(format!("VCD write error: {}", e)))?;
1456        }
1457        Ok(())
1458    }
1459
1460    /// Finish the current VCD file and continue the waveform in a new file,
1461    /// whose timestamps start over.
1462    #[napi]
1463    pub fn switch_vcd(&mut self, path: String) -> Result<()> {
1464        let writer = self.vcd_writer.as_mut().ok_or_else(|| {
1465            Error::from_reason("VCD output was not enabled when the simulator was created")
1466        })?;
1467        writer
1468            .restart(&path)
1469            .map_err(|e| Error::from_reason(format!("failed to start {path}: {e}")))
1470    }
1471
1472    /// Save the design state.
1473    #[napi]
1474    pub fn checkpoint(&self) -> Result<NativeSimulatorCheckpoint> {
1475        let b = self
1476            .backend
1477            .as_ref()
1478            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1479        Ok(NativeSimulatorCheckpoint {
1480            image: b.capture_state(self.state_fingerprint),
1481        })
1482    }
1483
1484    /// Return to the state saved in `checkpoint` and settle combinational
1485    /// logic.
1486    #[napi]
1487    pub fn restore(&mut self, checkpoint: &NativeSimulatorCheckpoint) -> Result<()> {
1488        let runtime_errors = self.runtime_errors.clone();
1489        let b = self
1490            .backend
1491            .as_mut()
1492            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1493        b.restore_state(&checkpoint.image, self.state_fingerprint)
1494            .map_err(|e| Error::from_reason(e.to_string()))?;
1495        if let Some(writer) = &mut self.vcd_writer {
1496            writer.rescan();
1497        }
1498        b.eval_comb()
1499            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1500    }
1501
1502    /// Save the value of every state object, by path, as state file bytes.
1503    #[napi]
1504    pub fn save_state(&mut self) -> Result<Buffer> {
1505        let runtime_errors = self.runtime_errors.clone();
1506        let b = self
1507            .backend
1508            .as_mut()
1509            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1510        b.eval_comb()
1511            .map_err(|e| napi_runtime_error(&runtime_errors, e))?;
1512        state_file_bytes(&b.save_state_file(&self.state_schema))
1513    }
1514
1515    /// Load state file bytes, matching objects by path, and settle
1516    /// combinational logic.
1517    #[napi]
1518    pub fn load_state(&mut self, bytes: Uint8Array) -> Result<()> {
1519        let file = parse_state_file(&bytes)?;
1520        let runtime_errors = self.runtime_errors.clone();
1521        let b = self
1522            .backend
1523            .as_mut()
1524            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1525        b.load_state_file(&self.state_schema, &file)
1526            .map_err(|mismatch| {
1527                Error::from_reason(celox::StateError::Mismatch(mismatch).to_string())
1528            })?;
1529        if let Some(writer) = &mut self.vcd_writer {
1530            writer.rescan();
1531        }
1532        b.eval_comb()
1533            .map_err(|e| napi_runtime_error(&runtime_errors, e))
1534    }
1535
1536    /// Return the simulator's stable memory region as a zero-copy `Uint8Array`.
1537    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
1538    #[napi]
1539    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
1540        let b = self
1541            .backend
1542            .as_mut()
1543            .ok_or_else(|| Error::from_reason("Simulator has been disposed"))?;
1544        let owner = b.memory_owner().ok_or_else(|| {
1545            Error::from_reason("Simulator backend does not expose owned shared memory")
1546        })?;
1547        let (ptr, _) = b.memory_as_mut_ptr();
1548        let stable_size = b.stable_region_size();
1549        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, move |_, _| drop(owner)) })
1550    }
1551
1552    /// Invalidate this handle and release its simulator resources.
1553    #[napi]
1554    pub fn dispose(&mut self) -> Result<()> {
1555        self.backend = None;
1556        if let Some(mut writer) = self.vcd_writer.take() {
1557            writer
1558                .flush()
1559                .map_err(|e| Error::from_reason(format!("VCD flush error: {e}")))?;
1560        }
1561        Ok(())
1562    }
1563}
1564
1565/// Low-level handle wrapping a `celox::Simulation`.
1566#[cfg(not(target_arch = "wasm32"))]
1567#[napi]
1568pub struct NativeSimulationHandle {
1569    sim: Option<celox::Simulation>,
1570    layout_json: String,
1571    events_json: String,
1572    hierarchy_json: String,
1573    warnings_json: String,
1574    stable_size: u32,
1575    total_size: u32,
1576    /// Default `maxSteps` for `waitUntil` / `waitForCycles`, sourced from
1577    /// `[simulation] max_steps` in `celox.toml`. `None` when not set.
1578    default_max_steps: Option<u32>,
1579}
1580
1581#[cfg(not(target_arch = "wasm32"))]
1582fn state_file_bytes(file: &celox::StateFile) -> Result<Buffer> {
1583    let mut bytes = Vec::new();
1584    file.write_to(&mut bytes)
1585        .map_err(|e| Error::from_reason(format!("failed to encode state: {e}")))?;
1586    Ok(bytes.into())
1587}
1588
1589#[cfg(not(target_arch = "wasm32"))]
1590fn parse_state_file(bytes: &[u8]) -> Result<celox::StateFile> {
1591    celox::StateFile::read_from(bytes).map_err(|e| Error::from_reason(e.to_string()))
1592}
1593
1594/// A periodic clock of a [`NativeSimulationHandle`].
1595#[cfg(not(target_arch = "wasm32"))]
1596#[napi(object)]
1597pub struct NapiClockPeriod {
1598    pub event_id: u32,
1599    pub period: f64,
1600}
1601
1602/// Saved state of a [`NativeSimulatorHandle`].
1603#[cfg(not(target_arch = "wasm32"))]
1604#[napi]
1605pub struct NativeSimulatorCheckpoint {
1606    image: celox::StateImage,
1607}
1608
1609#[cfg(not(target_arch = "wasm32"))]
1610#[napi]
1611impl NativeSimulatorCheckpoint {
1612    /// Size of the saved design state in bytes.
1613    #[napi(getter)]
1614    pub fn state_size(&self) -> u32 {
1615        self.image.len() as u32
1616    }
1617}
1618
1619/// Saved state of a [`NativeSimulationHandle`].
1620#[cfg(not(target_arch = "wasm32"))]
1621#[napi]
1622pub struct NativeSimulationCheckpoint {
1623    inner: celox::SimulationCheckpoint,
1624}
1625
1626#[cfg(not(target_arch = "wasm32"))]
1627#[napi]
1628impl NativeSimulationCheckpoint {
1629    /// Simulation time at which the checkpoint was taken.
1630    #[napi(getter)]
1631    pub fn time(&self) -> f64 {
1632        self.inner.time() as f64
1633    }
1634
1635    /// Size of the saved design state in bytes.
1636    #[napi(getter)]
1637    pub fn state_size(&self) -> u32 {
1638        self.inner.state_size() as u32
1639    }
1640}
1641
1642#[cfg(not(target_arch = "wasm32"))]
1643#[napi]
1644impl NativeSimulationHandle {
1645    /// Create a new timed simulation from Veryl source code.
1646    #[napi(constructor)]
1647    pub fn new(
1648        sources: Vec<NapiSourceFile>,
1649        top: String,
1650        options: Option<NapiOptions>,
1651    ) -> Result<Self> {
1652        let opts = parse_options(&options)?;
1653        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
1654            .into_iter()
1655            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
1656            .collect();
1657        append_extra_source(&mut src_pairs, &opts.extra_source);
1658        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
1659            .iter()
1660            .map(|(s, p)| (s.as_str(), p.as_path()))
1661            .collect();
1662        let builder = apply_options(celox::Simulation::from_sources(source_refs, &top), &opts);
1663        let sim = builder
1664            .build()
1665            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1666
1667        let warnings_json = format_warnings_json(sim.warnings());
1668        let signals = sim.named_signals();
1669        let events = sim.named_events();
1670        let hierarchy = sim.named_hierarchy();
1671        let (_, total_size) = sim.memory_as_ptr();
1672        let stable_size = sim.stable_region_size();
1673
1674        let layout_map = build_signal_layout(&signals, opts.four_state);
1675        let event_map = build_event_map(&events);
1676        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1677
1678        let layout_json = serde_json::to_string(&layout_map)
1679            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1680        let events_json = serde_json::to_string(&event_map)
1681            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1682        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1683            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1684
1685        Ok(Self {
1686            sim: Some(sim),
1687            layout_json,
1688            events_json,
1689            hierarchy_json,
1690            warnings_json,
1691            stable_size: stable_size as u32,
1692            total_size: total_size as u32,
1693            default_max_steps: None,
1694        })
1695    }
1696
1697    /// Create a timed simulation from a versioned external-frontend artifact.
1698    #[napi(factory)]
1699    pub fn from_frontend_artifact(
1700        artifact_json: String,
1701        options: Option<NapiOptions>,
1702    ) -> Result<Self> {
1703        let opts = parse_options(&options)?;
1704        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
1705            .map_err(|error| Error::from_reason(error.to_string()))?;
1706        let builder = apply_options(celox::Simulation::from_frontend(artifact), &opts);
1707        let sim = builder
1708            .build()
1709            .map_err(|error| Error::from_reason(error.to_string()))?;
1710
1711        let warnings_json = format_warnings_json(sim.warnings());
1712        let signals = sim.named_signals();
1713        let events = sim.named_events();
1714        let hierarchy = sim.named_hierarchy();
1715        let (_, total_size) = sim.memory_as_ptr();
1716        let stable_size = sim.stable_region_size();
1717        let layout_map = build_signal_layout(&signals, opts.four_state);
1718        let event_map = build_event_map(&events);
1719        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1720        let layout_json = serde_json::to_string(&layout_map)
1721            .map_err(|error| Error::from_reason(format!("Failed to serialize layout: {error}")))?;
1722        let events_json = serde_json::to_string(&event_map)
1723            .map_err(|error| Error::from_reason(format!("Failed to serialize events: {error}")))?;
1724        let hierarchy_json = serde_json::to_string(&hierarchy_node).map_err(|error| {
1725            Error::from_reason(format!("Failed to serialize hierarchy: {error}"))
1726        })?;
1727
1728        Ok(Self {
1729            sim: Some(sim),
1730            layout_json,
1731            events_json,
1732            hierarchy_json,
1733            warnings_json,
1734            stable_size: stable_size as u32,
1735            total_size: total_size as u32,
1736            default_max_steps: None,
1737        })
1738    }
1739
1740    /// Create a new timed simulation from a Veryl project directory.
1741    #[napi(factory)]
1742    pub fn from_project(
1743        project_path: String,
1744        top: String,
1745        options: Option<NapiOptions>,
1746    ) -> Result<Self> {
1747        let opts = parse_options(&options)?;
1748        let (mut sources, metadata, celox_cfg) = load_project_sources(&project_path)?;
1749        append_extra_source(&mut sources, &opts.extra_source);
1750        let source_refs: Vec<(&str, &std::path::Path)> = sources
1751            .iter()
1752            .map(|(s, p)| (s.as_str(), p.as_path()))
1753            .collect();
1754
1755        let builder = apply_options(
1756            celox::Simulation::from_sources(source_refs, &top).with_metadata(metadata),
1757            &opts,
1758        );
1759        let sim = builder
1760            .build()
1761            .map_err(|e| Error::from_reason(format!("{}", e)))?;
1762
1763        let warnings_json = format_warnings_json(sim.warnings());
1764        let signals = sim.named_signals();
1765        let events = sim.named_events();
1766        let hierarchy = sim.named_hierarchy();
1767        let (_, total_size) = sim.memory_as_ptr();
1768        let stable_size = sim.stable_region_size();
1769
1770        let layout_map = build_signal_layout(&signals, opts.four_state);
1771        let event_map = build_event_map(&events);
1772        let hierarchy_node = build_hierarchy_node(&hierarchy, opts.four_state);
1773
1774        let layout_json = serde_json::to_string(&layout_map)
1775            .map_err(|e| Error::from_reason(format!("Failed to serialize layout: {}", e)))?;
1776        let events_json = serde_json::to_string(&event_map)
1777            .map_err(|e| Error::from_reason(format!("Failed to serialize events: {}", e)))?;
1778        let hierarchy_json = serde_json::to_string(&hierarchy_node)
1779            .map_err(|e| Error::from_reason(format!("Failed to serialize hierarchy: {}", e)))?;
1780
1781        Ok(Self {
1782            sim: Some(sim),
1783            layout_json,
1784            events_json,
1785            hierarchy_json,
1786            warnings_json,
1787            stable_size: stable_size as u32,
1788            total_size: total_size as u32,
1789            default_max_steps: celox_cfg.simulation.max_steps,
1790        })
1791    }
1792
1793    /// Returns the signal layout as a JSON string.
1794    #[napi(getter)]
1795    pub fn layout_json(&self) -> String {
1796        self.layout_json.clone()
1797    }
1798
1799    /// Returns the event map as a JSON string.
1800    #[napi(getter)]
1801    pub fn events_json(&self) -> String {
1802        self.events_json.clone()
1803    }
1804
1805    /// Returns the instance hierarchy as a JSON string.
1806    #[napi(getter)]
1807    pub fn hierarchy_json(&self) -> String {
1808        self.hierarchy_json.clone()
1809    }
1810
1811    /// Returns compilation warnings as a JSON array of strings.
1812    #[napi(getter)]
1813    pub fn warnings_json(&self) -> String {
1814        self.warnings_json.clone()
1815    }
1816
1817    /// Returns the stable region size in bytes.
1818    #[napi(getter)]
1819    pub fn stable_size(&self) -> u32 {
1820        self.stable_size
1821    }
1822
1823    /// Returns the total memory size in bytes.
1824    #[napi(getter)]
1825    pub fn total_size(&self) -> u32 {
1826        self.total_size
1827    }
1828
1829    /// Returns the default `maxSteps` from `[simulation] max_steps` in `celox.toml`,
1830    /// or `null` if not configured.
1831    #[napi(getter)]
1832    pub fn default_max_steps(&self) -> Option<u32> {
1833        self.default_max_steps
1834    }
1835
1836    /// Register a clock by event ID.
1837    #[napi]
1838    pub fn add_clock(&mut self, event_id: u32, period: f64, initial_delay: f64) -> Result<()> {
1839        let sim = self
1840            .sim
1841            .as_mut()
1842            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1843        sim.try_add_clock_by_id(event_id, period as u64, initial_delay as u64)
1844            .map_err(|e| Error::from_reason(e.to_string()))
1845    }
1846
1847    /// Schedule a one-shot event by event ID.
1848    #[napi]
1849    pub fn schedule(&mut self, event_id: u32, time: f64, value: f64) -> Result<()> {
1850        let sim = self
1851            .sim
1852            .as_mut()
1853            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1854        sim.schedule_by_id(event_id, time as u64, value as u64)
1855            .map_err(|e| Error::from_reason(format!("{}", e)))
1856    }
1857
1858    /// Advance simulation until `end_time`.
1859    #[napi]
1860    pub fn run_until(&mut self, end_time: f64) -> Result<()> {
1861        let sim = self
1862            .sim
1863            .as_mut()
1864            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1865        sim.run_until(end_time as u64)
1866            .map_err(|e| Error::from_reason(format!("{}", e)))
1867    }
1868
1869    /// Advance to the next event. Returns the new time, or null if no events.
1870    #[napi]
1871    pub fn step(&mut self) -> Result<Option<f64>> {
1872        let sim = self
1873            .sim
1874            .as_mut()
1875            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1876        sim.step()
1877            .map(|opt| opt.map(|t| t as f64))
1878            .map_err(|e| Error::from_reason(format!("{}", e)))
1879    }
1880
1881    /// Returns the current simulation time.
1882    #[napi]
1883    pub fn time(&self) -> Result<f64> {
1884        let sim = self
1885            .sim
1886            .as_ref()
1887            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1888        Ok(sim.time() as f64)
1889    }
1890
1891    /// Returns the time of the next scheduled event, or null if none.
1892    #[napi]
1893    pub fn next_event_time(&self) -> Result<Option<f64>> {
1894        let sim = self
1895            .sim
1896            .as_ref()
1897            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1898        Ok(sim.next_event_time().map(|t| t as f64))
1899    }
1900
1901    /// Returns whether a process of the design requested the end of the
1902    /// simulation.
1903    #[napi]
1904    pub fn is_finished(&self) -> Result<bool> {
1905        let sim = self
1906            .sim
1907            .as_ref()
1908            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1909        Ok(sim.is_finished())
1910    }
1911
1912    /// Evaluate combinational logic.
1913    #[napi]
1914    pub fn eval_comb(&mut self) -> Result<()> {
1915        let sim = self
1916            .sim
1917            .as_mut()
1918            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1919        sim.eval_comb()
1920            .map_err(|e| Error::from_reason(format!("{}", e)))
1921    }
1922
1923    /// Write VCD dump at the given timestamp.
1924    #[napi]
1925    pub fn dump(&mut self, timestamp: f64) -> Result<()> {
1926        let sim = self
1927            .sim
1928            .as_mut()
1929            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1930        sim.try_dump(timestamp as u64)
1931            .map_err(|e| Error::from_reason(format!("VCD write error: {e}")))
1932    }
1933
1934    /// Finish the current VCD file and continue the waveform in a new file,
1935    /// whose timestamps start over.
1936    #[napi]
1937    pub fn switch_vcd(&mut self, path: String) -> Result<()> {
1938        let sim = self
1939            .sim
1940            .as_mut()
1941            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1942        sim.switch_vcd(&path)
1943            .map_err(|e| Error::from_reason(format!("failed to start {path}: {e}")))
1944    }
1945
1946    /// Save the design state, simulation time, clocks and pending events.
1947    #[napi]
1948    pub fn checkpoint(&self) -> Result<NativeSimulationCheckpoint> {
1949        let sim = self
1950            .sim
1951            .as_ref()
1952            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1953        let inner = sim
1954            .checkpoint()
1955            .map_err(|e| Error::from_reason(e.to_string()))?;
1956        Ok(NativeSimulationCheckpoint { inner })
1957    }
1958
1959    /// Return to the state saved in `checkpoint`.
1960    #[napi]
1961    pub fn restore(&mut self, checkpoint: &NativeSimulationCheckpoint) -> Result<()> {
1962        let sim = self
1963            .sim
1964            .as_mut()
1965            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1966        sim.restore(&checkpoint.inner)
1967            .map_err(|e| Error::from_reason(e.to_string()))?;
1968        // JS reads outputs straight from memory, so settle them now.
1969        sim.eval_comb()
1970            .map_err(|e| Error::from_reason(format!("{}", e)))
1971    }
1972
1973    /// Periodic clocks currently registered, including those restored from a
1974    /// checkpoint or state file.
1975    #[napi]
1976    pub fn clock_periods(&self) -> Result<Vec<NapiClockPeriod>> {
1977        let sim = self
1978            .sim
1979            .as_ref()
1980            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1981        Ok(sim
1982            .clock_periods()
1983            .into_iter()
1984            .map(|(event_id, period)| NapiClockPeriod {
1985                event_id: event_id as u32,
1986                period: period as f64,
1987            })
1988            .collect())
1989    }
1990
1991    /// Save the design state, time, clocks and pending events, by name, as
1992    /// state file bytes.
1993    #[napi]
1994    pub fn save_state(&mut self) -> Result<Buffer> {
1995        let sim = self
1996            .sim
1997            .as_mut()
1998            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
1999        // JS writes inputs straight to memory without marking the simulation
2000        // dirty, so settle combinational logic before saving it.
2001        sim.eval_comb()
2002            .map_err(|e| Error::from_reason(format!("{}", e)))?;
2003        let file = sim
2004            .save_state()
2005            .map_err(|e| Error::from_reason(e.to_string()))?;
2006        state_file_bytes(&file)
2007    }
2008
2009    /// Load state file bytes saved from a simulation. Combinational logic is
2010    /// settled as part of loading.
2011    #[napi]
2012    pub fn load_state(&mut self, bytes: Uint8Array) -> Result<()> {
2013        let file = parse_state_file(&bytes)?;
2014        let sim = self
2015            .sim
2016            .as_mut()
2017            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
2018        sim.load_state(&file)
2019            .map_err(|e| Error::from_reason(e.to_string()))
2020    }
2021
2022    /// Return the simulation's stable memory region as a zero-copy `Uint8Array`.
2023    /// JS can access `.buffer` to get the underlying `ArrayBuffer`.
2024    #[napi]
2025    pub fn shared_memory(&mut self) -> Result<Uint8Array> {
2026        let sim = self
2027            .sim
2028            .as_mut()
2029            .ok_or_else(|| Error::from_reason("Simulation has been disposed"))?;
2030        let owner = sim.memory_owner().ok_or_else(|| {
2031            Error::from_reason("Simulation backend does not expose owned shared memory")
2032        })?;
2033        let (ptr, _) = sim.memory_as_mut_ptr();
2034        let stable_size = sim.stable_region_size();
2035        Ok(unsafe { Uint8Array::with_external_data(ptr, stable_size, move |_, _| drop(owner)) })
2036    }
2037
2038    /// Invalidate this handle.
2039    #[napi]
2040    pub fn dispose(&mut self) -> Result<()> {
2041        if let Some(mut sim) = self.sim.take() {
2042            sim.flush_vcd()
2043                .map_err(|e| Error::from_reason(format!("VCD flush error: {e}")))?;
2044        }
2045        Ok(())
2046    }
2047}
2048
2049// ---------------------------------------------------------------------------
2050//  WASM32 NativeSimulatorHandle — compiles Veryl to WASM bytecode
2051// ---------------------------------------------------------------------------
2052
2053#[cfg(target_arch = "wasm32")]
2054#[napi]
2055pub struct NativeSimulatorHandle {
2056    program: celox::LaidOutProgram,
2057    four_state: bool,
2058    layout_json: String,
2059    events_json: String,
2060    hierarchy_json: String,
2061    warnings_json: String,
2062    stable_size: u32,
2063    total_size: u32,
2064}
2065
2066#[cfg(target_arch = "wasm32")]
2067impl NativeSimulatorHandle {
2068    /// Build an N-API/WASI handle directly from an in-memory frontend
2069    /// artifact.
2070    ///
2071    /// Frontend bindings can accept their own artifact type, lower it with
2072    /// `celox-frontend-sdk`, and call this Rust-only adapter API without a JSON
2073    /// serialization boundary.
2074    pub fn from_frontend(
2075        artifact: celox::FrontendArtifact,
2076        options: Option<NapiOptions>,
2077    ) -> Result<Self> {
2078        let opts = parse_options_common(&options)?;
2079        let trace_opts = celox::TraceOptions::default();
2080        let (program, warnings) = celox::compile_frontend_to_sir(
2081            &artifact,
2082            &opts.false_loops,
2083            &opts.true_loops,
2084            opts.four_state,
2085            &trace_opts,
2086            None,
2087            &opts.optimize_options,
2088        )
2089        .map_err(|error| Error::from_reason(error.to_string()))?;
2090
2091        let laid_out = program.into_laid_out(opts.four_state);
2092        let layout = laid_out.layout();
2093        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
2094        let events_json = Self::build_events_json(&laid_out);
2095        let warnings_json = format_warnings_json(&warnings);
2096        let stable_size = layout.total_size as u32;
2097        let total_size = layout.merged_total_size as u32;
2098
2099        Ok(Self {
2100            program: laid_out,
2101            four_state: opts.four_state,
2102            layout_json,
2103            events_json,
2104            hierarchy_json: "{}".to_string(),
2105            warnings_json,
2106            stable_size,
2107            total_size,
2108        })
2109    }
2110}
2111
2112#[cfg(target_arch = "wasm32")]
2113#[napi]
2114impl NativeSimulatorHandle {
2115    /// Compile Veryl source code and produce a WASM-oriented handle.
2116    ///
2117    /// Unlike the native (JIT) variant, this handle does NOT execute
2118    /// simulation directly. Instead it exposes `combWasmBytes()` and
2119    /// `eventWasmBytes(name)` for the TS runtime to instantiate in the
2120    /// browser via WebAssembly.
2121    #[napi(constructor)]
2122    pub fn new(
2123        sources: Vec<NapiSourceFile>,
2124        top: String,
2125        options: Option<NapiOptions>,
2126    ) -> Result<Self> {
2127        let opts = parse_options_common(&options)?;
2128        let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
2129            .into_iter()
2130            .map(|s| (s.content, std::path::PathBuf::from(s.path)))
2131            .collect();
2132        append_extra_source(&mut src_pairs, &opts.extra_source);
2133
2134        let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
2135            .iter()
2136            .map(|(s, p)| (s.as_str(), p.as_path()))
2137            .collect();
2138
2139        let trace_opts = celox::TraceOptions::default();
2140        let (program, warnings) = celox::compile_to_sir(
2141            &source_refs,
2142            &top,
2143            &opts
2144                .false_loops
2145                .iter()
2146                .map(|(f, t)| (f.clone(), t.clone()))
2147                .collect::<Vec<_>>(),
2148            &opts
2149                .true_loops
2150                .iter()
2151                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
2152                .collect::<Vec<_>>(),
2153            opts.four_state,
2154            &trace_opts,
2155            None,
2156            None,
2157            opts.clock_type,
2158            opts.reset_type,
2159            &opts.parameters,
2160            &opts.optimize_options,
2161        )
2162        .map_err(|e| Error::from_reason(format!("{}", e)))?;
2163
2164        let laid_out = program.into_laid_out(opts.four_state);
2165        let layout = laid_out.layout();
2166
2167        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
2168        let events_json = Self::build_events_json(&laid_out);
2169        let hierarchy_json = "{}".to_string(); // Hierarchy not available on wasm32
2170        let warnings_json = format_warnings_json(&warnings);
2171
2172        let stable_size = layout.total_size as u32;
2173        let total_size = layout.merged_total_size as u32;
2174        Ok(Self {
2175            program: laid_out,
2176            four_state: opts.four_state,
2177            layout_json,
2178            events_json,
2179            hierarchy_json,
2180            warnings_json,
2181            stable_size,
2182            total_size,
2183        })
2184    }
2185
2186    /// Create a WASM-oriented handle from a versioned external-frontend artifact.
2187    #[napi(factory)]
2188    pub fn from_frontend_artifact(
2189        artifact_json: String,
2190        options: Option<NapiOptions>,
2191    ) -> Result<Self> {
2192        let artifact = celox::FrontendArtifact::from_json(&artifact_json)
2193            .map_err(|error| Error::from_reason(error.to_string()))?;
2194        Self::from_frontend(artifact, options)
2195    }
2196
2197    /// Create a new simulator from a Veryl project directory.
2198    #[napi(factory)]
2199    pub fn from_project(
2200        project_path: String,
2201        top: String,
2202        options: Option<NapiOptions>,
2203    ) -> Result<Self> {
2204        let opts = parse_options_common(&options)?;
2205        let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
2206        append_extra_source(&mut sources, &opts.extra_source);
2207
2208        let source_refs: Vec<(&str, &std::path::Path)> = sources
2209            .iter()
2210            .map(|(s, p)| (s.as_str(), p.as_path()))
2211            .collect();
2212
2213        let trace_opts = celox::TraceOptions::default();
2214        let (program, warnings) = celox::compile_to_sir(
2215            &source_refs,
2216            &top,
2217            &opts
2218                .false_loops
2219                .iter()
2220                .map(|(f, t)| (f.clone(), t.clone()))
2221                .collect::<Vec<_>>(),
2222            &opts
2223                .true_loops
2224                .iter()
2225                .map(|(f, t, m)| (f.clone(), t.clone(), *m))
2226                .collect::<Vec<_>>(),
2227            opts.four_state,
2228            &trace_opts,
2229            None,
2230            Some(metadata),
2231            opts.clock_type,
2232            opts.reset_type,
2233            &opts.parameters,
2234            &opts.optimize_options,
2235        )
2236        .map_err(|e| Error::from_reason(format!("{}", e)))?;
2237
2238        let laid_out = program.into_laid_out(opts.four_state);
2239        let layout = laid_out.layout();
2240
2241        let layout_json = Self::build_layout_json(&laid_out, layout, opts.four_state);
2242        let events_json = Self::build_events_json(&laid_out);
2243        let hierarchy_json = "{}".to_string();
2244        let warnings_json = format_warnings_json(&warnings);
2245
2246        let stable_size = layout.total_size as u32;
2247        let total_size = layout.merged_total_size as u32;
2248        Ok(Self {
2249            program: laid_out,
2250            four_state: opts.four_state,
2251            layout_json,
2252            events_json,
2253            hierarchy_json,
2254            warnings_json,
2255            stable_size,
2256            total_size,
2257        })
2258    }
2259
2260    /// Returns the signal layout as a JSON string.
2261    #[napi(getter)]
2262    pub fn layout_json(&self) -> String {
2263        self.layout_json.clone()
2264    }
2265
2266    /// Returns the event map as a JSON string.
2267    #[napi(getter)]
2268    pub fn events_json(&self) -> String {
2269        self.events_json.clone()
2270    }
2271
2272    /// Byte ranges whose value and mask planes must start as unknown (X).
2273    #[napi(getter)]
2274    pub fn four_state_init_regions_json(&self) -> String {
2275        Self::build_four_state_init_regions_json(
2276            &self.program,
2277            self.program.layout(),
2278            self.four_state,
2279        )
2280    }
2281
2282    /// Return a complete initialized memory image for the TypeScript WASM bridge.
2283    #[napi]
2284    pub fn initial_memory_bytes(&self) -> Vec<u8> {
2285        Self::build_initial_memory_bytes(&self.program, self.program.layout(), self.four_state)
2286    }
2287
2288    /// Byte offset where the checkpointable design state starts; it ends at
2289    /// `stableSize`. The bytes before it hold host data and are never saved.
2290    #[napi(getter)]
2291    pub fn state_offset(&self) -> u32 {
2292        celox::STATE_HEADER_SIZE as u32
2293    }
2294
2295    /// Identity of the checkpointable state layout. Memory images can be
2296    /// exchanged between handles with equal fingerprints.
2297    #[napi(getter)]
2298    pub fn state_fingerprint(&self) -> String {
2299        format!("{:016x}", self.program.state_fingerprint())
2300    }
2301
2302    /// Returns the instance hierarchy as a JSON string.
2303    #[napi(getter)]
2304    pub fn hierarchy_json(&self) -> String {
2305        self.hierarchy_json.clone()
2306    }
2307
2308    /// Returns compilation warnings as a JSON array of strings.
2309    #[napi(getter)]
2310    pub fn warnings_json(&self) -> String {
2311        self.warnings_json.clone()
2312    }
2313
2314    /// Returns the stable region size in bytes.
2315    #[napi(getter)]
2316    pub fn stable_size(&self) -> u32 {
2317        self.stable_size
2318    }
2319
2320    /// Returns the total memory size in bytes.
2321    #[napi(getter)]
2322    pub fn total_size(&self) -> u32 {
2323        self.total_size
2324    }
2325
2326    /// Returns the WASM module bytes for eval_comb (combinational logic evaluation).
2327    #[napi]
2328    pub fn comb_wasm_bytes(&self) -> Vec<u8> {
2329        let wasm = celox::wasm_codegen::compile_units(
2330            &self.program.sir.eval_comb,
2331            self.program.layout(),
2332            self.four_state,
2333            false,
2334        );
2335        wasm.bytes
2336    }
2337
2338    /// Returns the WASM module bytes for a specific clock/reset event.
2339    ///
2340    /// `event_name` should match a clock or reset port name (e.g. "clk", "rst").
2341    #[napi]
2342    pub fn event_wasm_bytes(&self, event_name: String) -> Result<Vec<u8>> {
2343        for (addr, units) in &self.program.sir.eval_apply_ffs {
2344            let event_path = self.program.get_path(addr);
2345            if event_path == event_name {
2346                let wasm = celox::wasm_codegen::compile_units(
2347                    units,
2348                    self.program.layout(),
2349                    self.four_state,
2350                    false,
2351                );
2352                return Ok(wasm.bytes);
2353            }
2354        }
2355
2356        Err(Error::from_reason(format!(
2357            "Event '{}' not found. Available events: {}",
2358            event_name,
2359            self.program
2360                .sir
2361                .eval_apply_ffs
2362                .keys()
2363                .map(|addr| self.program.get_path(addr))
2364                .collect::<Vec<_>>()
2365                .join(", ")
2366        )))
2367    }
2368
2369    /// No-op on wasm32 (no native resources to release).
2370    #[napi]
2371    pub fn dispose(&mut self) {}
2372}
2373
2374#[cfg(target_arch = "wasm32")]
2375impl NativeSimulatorHandle {
2376    fn write_memory_bit(memory: &mut [u8], byte: usize, bit: usize, value: bool) {
2377        let mask = 1u8 << bit;
2378        if value {
2379            memory[byte] |= mask;
2380        } else {
2381            memory[byte] &= !mask;
2382        }
2383    }
2384
2385    fn byte_bit(bytes: &[u8], bit: usize) -> bool {
2386        bytes
2387            .get(bit / 8)
2388            .is_some_and(|byte| byte & (1u8 << (bit % 8)) != 0)
2389    }
2390
2391    fn build_initial_memory_bytes(
2392        program: &celox::LaidOutProgram,
2393        layout: &celox::MemoryLayout,
2394        four_state: bool,
2395    ) -> Vec<u8> {
2396        let mut memory = vec![0u8; layout.merged_total_size];
2397
2398        if four_state {
2399            for (address, &offset) in &layout.offsets {
2400                if layout.is_4states.get(address).copied().unwrap_or(false) {
2401                    let plane_size = layout.plane_size(address);
2402                    memory[offset..offset + plane_size * 2].fill(0xff);
2403                }
2404            }
2405            for (address, &relative_offset) in &layout.working_offsets {
2406                if layout.is_4states.get(address).copied().unwrap_or(false) {
2407                    let offset = layout.working_base_offset + relative_offset;
2408                    let plane_size = layout.plane_size(address);
2409                    memory[offset..offset + plane_size * 2].fill(0xff);
2410                }
2411            }
2412        }
2413
2414        for initial in &program.design.initial_state {
2415            let Some(&offset) = layout.offsets.get(&initial.address) else {
2416                continue;
2417            };
2418            let width = layout.widths[&initial.address];
2419            let plane_size = layout.plane_size(&initial.address);
2420            let write_mask = four_state
2421                && layout
2422                    .is_4states
2423                    .get(&initial.address)
2424                    .copied()
2425                    .unwrap_or(false);
2426
2427            match &initial.data {
2428                celox_design::InitialStateData::Packed {
2429                    value,
2430                    mask,
2431                    written_mask,
2432                } => {
2433                    let value = value.to_bytes_le();
2434                    let mask = mask.to_bytes_le();
2435                    let written_mask = written_mask.to_bytes_le();
2436                    for bit in 0..width {
2437                        if !Self::byte_bit(&written_mask, bit) {
2438                            continue;
2439                        }
2440                        let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
2441                        let is_unknown = Self::byte_bit(&mask, bit);
2442                        Self::write_memory_bit(
2443                            &mut memory,
2444                            offset + byte,
2445                            intra,
2446                            Self::byte_bit(&value, bit) && (write_mask || !is_unknown),
2447                        );
2448                        if write_mask {
2449                            Self::write_memory_bit(
2450                                &mut memory,
2451                                offset + plane_size + byte,
2452                                intra,
2453                                is_unknown,
2454                            );
2455                        }
2456                    }
2457                }
2458                celox_design::InitialStateData::Writes(runs) => {
2459                    for run in runs {
2460                        for relative_bit in 0..run.bit_width {
2461                            let bit = run.bit_offset + relative_bit;
2462                            if bit >= width {
2463                                break;
2464                            }
2465                            let (byte, intra) = layout.map_static_bit_offset(&initial.address, bit);
2466                            Self::write_memory_bit(
2467                                &mut memory,
2468                                offset + byte,
2469                                intra,
2470                                Self::byte_bit(&run.value_bytes, relative_bit),
2471                            );
2472                            if write_mask {
2473                                Self::write_memory_bit(
2474                                    &mut memory,
2475                                    offset + plane_size + byte,
2476                                    intra,
2477                                    Self::byte_bit(&run.mask_bytes, relative_bit),
2478                                );
2479                            }
2480                        }
2481                    }
2482                }
2483            }
2484        }
2485
2486        memory
2487    }
2488
2489    fn build_four_state_init_regions_json(
2490        program: &celox::LaidOutProgram,
2491        layout: &celox::MemoryLayout,
2492        four_state: bool,
2493    ) -> String {
2494        if !four_state {
2495            return "[]".to_string();
2496        }
2497
2498        let mut regions = Vec::new();
2499        for (addr, &offset) in &layout.offsets {
2500            if program
2501                .design
2502                .state_objects
2503                .get(addr)
2504                .is_some_and(|metadata| metadata.is_4state)
2505            {
2506                regions.push((offset, layout.plane_size(addr)));
2507            }
2508        }
2509        for (addr, &relative_offset) in &layout.working_offsets {
2510            if program
2511                .design
2512                .state_objects
2513                .get(addr)
2514                .is_some_and(|metadata| metadata.is_4state)
2515            {
2516                regions.push((
2517                    layout.working_base_offset + relative_offset,
2518                    layout.plane_size(addr),
2519                ));
2520            }
2521        }
2522        regions.sort_unstable();
2523
2524        serde_json::to_string(&regions).unwrap_or_else(|_| "[]".to_string())
2525    }
2526
2527    /// Build signal layout JSON from finalized SIR and MemoryLayout.
2528    /// Mirrors the layout format from celox-wasm.
2529    fn build_layout_json(
2530        program: &celox::LaidOutProgram,
2531        layout: &celox::MemoryLayout,
2532        four_state: bool,
2533    ) -> String {
2534        use std::collections::BTreeMap;
2535
2536        let mut layout_map: BTreeMap<String, serde_json::Value> = BTreeMap::new();
2537
2538        for addr in program.design.state_objects.keys() {
2539            let Some(variable) = program.design.variable(addr) else {
2540                continue;
2541            };
2542            let Some(instance) = program.design.instance(addr.instance_id) else {
2543                continue;
2544            };
2545            if !instance.resolves_path_to(&variable.path, *addr) {
2546                continue;
2547            }
2548            let metadata = &program.design.state_objects[addr];
2549            let Some(&offset) = layout.offsets.get(addr) else {
2550                continue;
2551            };
2552            let name = program.get_path(addr);
2553            let total_width = layout.widths.get(addr).copied().unwrap_or(0);
2554            let (width, array_dims) = if metadata.array_dims.is_empty() {
2555                (total_width, None)
2556            } else {
2557                let element_count = metadata.array_dims.iter().product::<usize>();
2558                (total_width / element_count, Some(&metadata.array_dims))
2559            };
2560            let byte_size = celox::get_byte_size(width);
2561            let mut entry = serde_json::json!({
2562                "offset": offset,
2563                "width": width,
2564                "byte_size": byte_size,
2565                "is_4state": four_state && metadata.is_4state,
2566                "direction": layout::direction_str(variable.var_kind),
2567                "type_kind": layout::type_kind_str(metadata.type_kind),
2568            });
2569            if let Some(array_dims) = array_dims {
2570                entry["array_dims"] = serde_json::json!(array_dims);
2571            }
2572            layout_map.insert(name, entry);
2573        }
2574
2575        serde_json::to_string(&layout_map).unwrap_or_else(|_| "{}".to_string())
2576    }
2577
2578    /// Build events JSON from finalized SIR.
2579    fn build_events_json(program: &celox::LaidOutProgram) -> String {
2580        use std::collections::BTreeMap;
2581
2582        let mut events: BTreeMap<String, usize> = BTreeMap::new();
2583
2584        for (next_id, addr) in program.sir.eval_apply_ffs.keys().enumerate() {
2585            let name = program.get_path(addr);
2586            events.insert(name, next_id);
2587        }
2588
2589        serde_json::to_string(&events).unwrap_or_else(|_| "{}".to_string())
2590    }
2591}
2592
2593/// Convert an `AnalyzerError` to structured `JsonDiagnostic`s.
2594/// `all_sources` maps path → source content for offset → line:col conversion.
2595fn analyzer_error_to_diagnostics(
2596    err: &veryl_analyzer::AnalyzerError,
2597    all_sources: &[(String, String)],
2598    is_error: bool,
2599) -> Vec<celox_ts_gen::JsonDiagnostic> {
2600    use miette::Diagnostic as _;
2601
2602    let severity = if is_error {
2603        celox_ts_gen::DiagnosticSeverity::Error
2604    } else {
2605        celox_ts_gen::DiagnosticSeverity::Warning
2606    };
2607
2608    let message = format!("{err}");
2609    let help = err.help().map(|h| h.to_string());
2610    let url = err.url().map(|u| u.to_string());
2611
2612    let labels: Vec<_> = err.labels().map(|l| l.collect()).unwrap_or_default();
2613
2614    if labels.is_empty() {
2615        return vec![celox_ts_gen::JsonDiagnostic {
2616            severity,
2617            message,
2618            file: String::new(),
2619            line: 1,
2620            column: 1,
2621            end_line: None,
2622            end_column: None,
2623            help,
2624            url,
2625        }];
2626    }
2627
2628    labels
2629        .into_iter()
2630        .map(|label| {
2631            let offset = label.offset();
2632            let len = label.len();
2633
2634            // Find which source file this offset belongs to
2635            // AnalyzerError uses global offsets across concatenated sources,
2636            // but typically the error_location is within a single file.
2637            // Try each source to find line:col.
2638            let mut file = String::new();
2639            let mut line = 1usize;
2640            let mut col = 1usize;
2641            let mut end_line = None;
2642            let mut end_col = None;
2643
2644            for (path, content) in all_sources {
2645                // miette offsets are per-file when Parser::parse is given the source
2646                let mut cur_line = 1;
2647                let mut cur_col = 1;
2648                let mut found = false;
2649
2650                for (i, ch) in content.char_indices() {
2651                    if i == offset {
2652                        file = path.clone();
2653                        line = cur_line;
2654                        col = cur_col;
2655                        found = true;
2656                    }
2657                    if found && i == offset + len {
2658                        end_line = Some(cur_line);
2659                        end_col = Some(cur_col);
2660                        break;
2661                    }
2662                    if ch == '\n' {
2663                        cur_line += 1;
2664                        cur_col = 1;
2665                    } else {
2666                        cur_col += 1;
2667                    }
2668                }
2669
2670                if found {
2671                    if end_line.is_none() {
2672                        end_line = Some(cur_line);
2673                        end_col = Some(cur_col);
2674                    }
2675                    break;
2676                }
2677            }
2678
2679            celox_ts_gen::JsonDiagnostic {
2680                severity: severity.clone(),
2681                message: label.label().unwrap_or(&message).to_string(),
2682                file,
2683                line,
2684                column: col,
2685                end_line,
2686                end_column: end_col,
2687                help: help.clone(),
2688                url: url.clone(),
2689            }
2690        })
2691        .collect()
2692}
2693
2694/// Format analyzer errors with accumulated warnings for gen_ts error messages.
2695fn format_errors_with_warnings(
2696    pass_label: &str,
2697    errors: &[&veryl_analyzer::AnalyzerError],
2698    warnings: &[veryl_analyzer::AnalyzerError],
2699) -> String {
2700    let error_msgs: Vec<String> = errors
2701        .iter()
2702        .map(|e| celox::render_diagnostic(*e))
2703        .collect();
2704    let mut msg = format!("Errors in {pass_label}: {}", error_msgs.join("; "));
2705    if !warnings.is_empty() {
2706        let warning_msgs: Vec<String> = warnings
2707            .iter()
2708            .map(|w| celox::render_diagnostic(w))
2709            .collect();
2710        msg.push_str("\n\n--- warnings ---\n\n");
2711        msg.push_str(&warning_msgs.join("\n"));
2712    }
2713    msg
2714}
2715
2716/// Clear the process-global JIT compilation cache.
2717///
2718/// Call this when source files have changed and cached compiled code may be stale.
2719#[cfg(not(target_arch = "wasm32"))]
2720#[napi]
2721pub fn clear_jit_cache() {
2722    let mut cache = JIT_CACHE.lock().unwrap_or_else(|e| e.into_inner());
2723    cache.clear();
2724}
2725
2726/// Stub for wasm32: no JIT cache to clear.
2727#[cfg(target_arch = "wasm32")]
2728#[napi]
2729pub fn clear_jit_cache() {}
2730
2731// ---------------------------------------------------------------------------
2732//  Native testbench execution
2733// ---------------------------------------------------------------------------
2734
2735/// Result of a single `$assert` evaluation in a native testbench.
2736#[cfg(not(target_arch = "wasm32"))]
2737#[napi(object)]
2738pub struct NapiAssertionResult {
2739    pub passed: bool,
2740    pub message: Option<Uint8Array>,
2741    pub file: Option<String>,
2742    pub line: Option<u32>,
2743    pub column: Option<u32>,
2744}
2745
2746/// Detailed result of running a native testbench.
2747#[cfg(not(target_arch = "wasm32"))]
2748#[napi(object)]
2749pub struct NapiTestResult {
2750    pub passed: bool,
2751    pub assertions: Vec<NapiAssertionResult>,
2752    pub error: Option<String>,
2753}
2754
2755#[cfg(not(target_arch = "wasm32"))]
2756fn convert_test_result(r: celox::TestResultDetailed) -> NapiTestResult {
2757    NapiTestResult {
2758        passed: r.passed,
2759        error: r.error,
2760        assertions: r
2761            .assertions
2762            .into_iter()
2763            .map(|a| {
2764                let (file, line, column) = match a.location {
2765                    Some(loc) => (Some(loc.file), Some(loc.line), Some(loc.column)),
2766                    None => (None, None, None),
2767                };
2768                NapiAssertionResult {
2769                    passed: a.passed,
2770                    message: a.message.map(Uint8Array::from),
2771                    file,
2772                    line,
2773                    column,
2774                }
2775            })
2776            .collect(),
2777    }
2778}
2779
2780#[cfg(not(target_arch = "wasm32"))]
2781#[napi(object)]
2782pub struct NapiInjectedValue {
2783    pub name: Option<String>,
2784    pub bits: Option<BigInt>,
2785    pub mask_xz: Option<BigInt>,
2786    pub width: Option<u32>,
2787    pub string_value: Option<String>,
2788}
2789
2790#[cfg(not(target_arch = "wasm32"))]
2791#[napi(object)]
2792pub struct NapiInjectedCall {
2793    pub instance: String,
2794    pub phase: String,
2795    pub method: Option<String>,
2796    pub inputs: Vec<NapiInjectedValue>,
2797    pub params: Vec<NapiInjectedValue>,
2798    pub ports: Vec<NapiInjectedPort>,
2799    pub args: Vec<NapiInjectedValue>,
2800    pub cycle: BigInt,
2801    pub time: BigInt,
2802    pub seed: BigInt,
2803    pub fired_clock: Option<String>,
2804    pub four_state: bool,
2805}
2806
2807#[cfg(not(target_arch = "wasm32"))]
2808#[napi(object)]
2809pub struct NapiInjectedPort {
2810    pub name: String,
2811    pub direction: String,
2812    pub role: Option<String>,
2813    pub width: u32,
2814}
2815
2816#[cfg(not(target_arch = "wasm32"))]
2817#[napi(object)]
2818pub struct NapiInjectedResult {
2819    pub outputs: Option<Vec<NapiInjectedValue>>,
2820    pub return_value: Option<NapiInjectedValue>,
2821    pub failures: Option<Vec<String>>,
2822    pub logs: Option<Vec<String>>,
2823    pub finish: Option<bool>,
2824}
2825
2826#[cfg(not(target_arch = "wasm32"))]
2827#[napi(object, object_to_js = false)]
2828pub struct NapiInjectedComponent {
2829    pub name: String,
2830    pub manifest: String,
2831    pub handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2832}
2833
2834#[cfg(not(target_arch = "wasm32"))]
2835struct NapiInjectedHandler {
2836    env: Env,
2837    handler: FunctionRef<NapiInjectedCall, NapiInjectedResult>,
2838}
2839
2840// Injected callbacks are only accepted by the synchronous runTest APIs and
2841// are invoked on the JS thread which supplied this Env. The core trait is
2842// Send + Sync because compiled component hooks may otherwise be movable.
2843#[cfg(not(target_arch = "wasm32"))]
2844unsafe impl Send for NapiInjectedHandler {}
2845#[cfg(not(target_arch = "wasm32"))]
2846unsafe impl Sync for NapiInjectedHandler {}
2847
2848#[cfg(not(target_arch = "wasm32"))]
2849fn napi_bigint(value: u64) -> BigInt {
2850    BigInt {
2851        sign_bit: false,
2852        words: vec![value],
2853    }
2854}
2855
2856#[cfg(not(target_arch = "wasm32"))]
2857fn to_napi_injected_value(name: Option<String>, value: celox::InjectedValue) -> NapiInjectedValue {
2858    match value {
2859        celox::InjectedValue::Bits {
2860            words,
2861            mask_xz,
2862            width,
2863        } => NapiInjectedValue {
2864            name,
2865            bits: Some(BigInt {
2866                sign_bit: false,
2867                words,
2868            }),
2869            mask_xz: Some(BigInt {
2870                sign_bit: false,
2871                words: mask_xz,
2872            }),
2873            width: Some(width),
2874            string_value: None,
2875        },
2876        celox::InjectedValue::String(value) => NapiInjectedValue {
2877            name,
2878            bits: None,
2879            mask_xz: None,
2880            width: None,
2881            string_value: Some(value),
2882        },
2883        celox::InjectedValue::Unit => NapiInjectedValue {
2884            name,
2885            bits: None,
2886            mask_xz: None,
2887            width: None,
2888            string_value: None,
2889        },
2890    }
2891}
2892
2893#[cfg(not(target_arch = "wasm32"))]
2894fn from_napi_injected_value(
2895    value: NapiInjectedValue,
2896) -> std::result::Result<celox::InjectedValue, String> {
2897    if let Some(bits) = value.bits {
2898        if bits.sign_bit || value.mask_xz.as_ref().is_some_and(|mask| mask.sign_bit) {
2899            return Err("component callback values cannot be negative".into());
2900        }
2901        let width = value
2902            .width
2903            .ok_or_else(|| "component callback bit value has no width".to_string())?;
2904        return Ok(celox::InjectedValue::Bits {
2905            words: bits.words,
2906            mask_xz: value.mask_xz.map(|mask| mask.words).unwrap_or_default(),
2907            width,
2908        });
2909    }
2910    Ok(match value.string_value {
2911        Some(value) => celox::InjectedValue::String(value),
2912        None => celox::InjectedValue::Unit,
2913    })
2914}
2915
2916#[cfg(not(target_arch = "wasm32"))]
2917impl celox::InjectedComponentHandler for NapiInjectedHandler {
2918    fn call(
2919        &self,
2920        call: celox::InjectedCall,
2921    ) -> std::result::Result<celox::InjectedResult, String> {
2922        let (phase, method, args) = match call.hook {
2923            celox::InjectedHook::Create => ("create", None, Vec::new()),
2924            celox::InjectedHook::Init => ("init", None, Vec::new()),
2925            celox::InjectedHook::Reset => ("reset", None, Vec::new()),
2926            celox::InjectedHook::Clock => ("clock", None, Vec::new()),
2927            celox::InjectedHook::Finish => ("finish", None, Vec::new()),
2928            celox::InjectedHook::Method { name, args } => ("method", Some(name), args),
2929        };
2930        let request = NapiInjectedCall {
2931            instance: call.instance,
2932            phase: phase.into(),
2933            method,
2934            inputs: call
2935                .inputs
2936                .into_iter()
2937                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2938                .collect(),
2939            params: call
2940                .params
2941                .into_iter()
2942                .map(|value| to_napi_injected_value(Some(value.name), value.value))
2943                .collect(),
2944            ports: call
2945                .ports
2946                .into_iter()
2947                .map(|port| NapiInjectedPort {
2948                    name: port.name,
2949                    direction: port.direction,
2950                    role: port.role,
2951                    width: port.width,
2952                })
2953                .collect(),
2954            args: args
2955                .into_iter()
2956                .map(|value| to_napi_injected_value(None, value))
2957                .collect(),
2958            cycle: napi_bigint(call.cycle),
2959            time: napi_bigint(call.time),
2960            seed: napi_bigint(call.seed),
2961            fired_clock: call.fired_clock,
2962            four_state: call.four_state,
2963        };
2964        let result = self
2965            .handler
2966            .borrow_back(&self.env)
2967            .and_then(|handler| handler.call(request))
2968            .map_err(|error| error.to_string())?;
2969        let outputs = result
2970            .outputs
2971            .unwrap_or_default()
2972            .into_iter()
2973            .map(|value| {
2974                let name = value
2975                    .name
2976                    .clone()
2977                    .ok_or_else(|| "component callback output has no name".to_string())?;
2978                Ok(celox::InjectedNamedValue {
2979                    name,
2980                    value: from_napi_injected_value(value)?,
2981                })
2982            })
2983            .collect::<std::result::Result<Vec<_>, String>>()?;
2984        Ok(celox::InjectedResult {
2985            outputs,
2986            return_value: result
2987                .return_value
2988                .map(from_napi_injected_value)
2989                .transpose()?,
2990            failures: result.failures.unwrap_or_default(),
2991            logs: result.logs.unwrap_or_default(),
2992            finish: result.finish.unwrap_or(false),
2993        })
2994    }
2995}
2996
2997#[cfg(not(target_arch = "wasm32"))]
2998fn injected_components(
2999    env: Env,
3000    definitions: Option<Vec<NapiInjectedComponent>>,
3001) -> Result<celox::InjectedComponents> {
3002    let mut components = celox::InjectedComponents::new();
3003    for definition in definitions.unwrap_or_default() {
3004        components
3005            .insert(
3006                definition.name,
3007                &definition.manifest,
3008                Arc::new(NapiInjectedHandler {
3009                    env,
3010                    handler: definition.handler,
3011                }),
3012            )
3013            .map_err(Error::from_reason)?;
3014    }
3015    Ok(components)
3016}
3017
3018/// Run a native testbench from Veryl source code.
3019///
3020/// Compiles the given sources and runs the `#[test]` module specified by `top`,
3021/// returning assertion results observed before that test finishes or stops on a
3022/// fatal failure.
3023#[cfg(not(target_arch = "wasm32"))]
3024#[napi]
3025pub fn run_test(
3026    env: Env,
3027    sources: Vec<NapiSourceFile>,
3028    top: String,
3029    options: Option<NapiOptions>,
3030    components: Option<Vec<NapiInjectedComponent>>,
3031) -> Result<NapiTestResult> {
3032    let opts = parse_options(&options)?;
3033    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
3034        .into_iter()
3035        .map(|s| (s.content, std::path::PathBuf::from(s.path)))
3036        .collect();
3037    append_extra_source(&mut src_pairs, &opts.extra_source);
3038
3039    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
3040        .iter()
3041        .map(|(s, p)| (s.as_str(), p.as_path()))
3042        .collect();
3043    let builder = apply_options(celox::Simulator::from_sources(source_refs, &top), &opts)
3044        .with_injected_components(injected_components(env, components)?);
3045    let result = builder
3046        .run_test_detailed()
3047        .map_err(|e| Error::from_reason(format!("{e}")))?;
3048    Ok(convert_test_result(result))
3049}
3050
3051/// Run a Veryl native testbench against an external frontend artifact.
3052#[cfg(not(target_arch = "wasm32"))]
3053#[napi]
3054pub fn run_test_with_frontend_artifact(
3055    env: Env,
3056    artifact_json: String,
3057    sources: Vec<NapiSourceFile>,
3058    top: String,
3059    options: Option<NapiOptions>,
3060    components: Option<Vec<NapiInjectedComponent>>,
3061) -> Result<NapiTestResult> {
3062    let opts = parse_options(&options)?;
3063    let artifact = celox::FrontendArtifact::from_json(&artifact_json)
3064        .map_err(|error| Error::from_reason(error.to_string()))?;
3065    let mut src_pairs: Vec<(String, std::path::PathBuf)> = sources
3066        .into_iter()
3067        .map(|source| (source.content, std::path::PathBuf::from(source.path)))
3068        .collect();
3069    append_extra_source(&mut src_pairs, &opts.extra_source);
3070    let source_refs: Vec<(&str, &std::path::Path)> = src_pairs
3071        .iter()
3072        .map(|(source, path)| (source.as_str(), path.as_path()))
3073        .collect();
3074    let builder = apply_options(
3075        celox::Simulator::from_frontend_with_testbench(artifact, source_refs, &top),
3076        &opts,
3077    )
3078    .with_injected_components(injected_components(env, components)?);
3079    let result = builder
3080        .run_test_detailed()
3081        .map_err(|error| Error::from_reason(error.to_string()))?;
3082    Ok(convert_test_result(result))
3083}
3084
3085/// Run a native testbench from a Veryl project directory.
3086///
3087/// Searches upward from `project_path` for `Veryl.toml`, gathers all
3088/// `.veryl` source files, and runs the `#[test]` module specified by `top`.
3089#[cfg(not(target_arch = "wasm32"))]
3090#[napi]
3091pub fn run_test_from_project(
3092    env: Env,
3093    project_path: String,
3094    top: String,
3095    options: Option<NapiOptions>,
3096    components: Option<Vec<NapiInjectedComponent>>,
3097) -> Result<NapiTestResult> {
3098    let opts = parse_options(&options)?;
3099    let (mut sources, metadata, _celox_cfg) = load_project_sources(&project_path)?;
3100    append_extra_source(&mut sources, &opts.extra_source);
3101
3102    let source_refs: Vec<(&str, &std::path::Path)> = sources
3103        .iter()
3104        .map(|(s, p)| (s.as_str(), p.as_path()))
3105        .collect();
3106    let builder = apply_options(
3107        celox::Simulator::from_sources(source_refs, &top).with_metadata(metadata),
3108        &opts,
3109    )
3110    .with_injected_components(injected_components(env, components)?);
3111    let result = builder
3112        .run_test_detailed()
3113        .map_err(|e| Error::from_reason(format!("{e}")))?;
3114    Ok(convert_test_result(result))
3115}
3116
3117// ---------------------------------------------------------------------------
3118//  TypeScript type generation
3119// ---------------------------------------------------------------------------
3120
3121#[napi(object)]
3122pub struct NapiInjectedManifest {
3123    pub name: String,
3124    pub manifest: String,
3125}
3126
3127fn inject_analyzer_components(definitions: Option<Vec<NapiInjectedManifest>>) -> Result<()> {
3128    let definitions = definitions.unwrap_or_default();
3129    let names: Vec<_> = definitions
3130        .iter()
3131        .map(|definition| definition.name.as_str())
3132        .collect();
3133    veryl_analyzer::tb_component::insert_external_components(&names);
3134    for definition in definitions {
3135        let manifest =
3136            veryl_metadata::ComponentManifest::parse(&definition.manifest).ok_or_else(|| {
3137                Error::from_reason(format!(
3138                    "manifest of injected component `{}` cannot be parsed",
3139                    definition.name
3140                ))
3141            })?;
3142        veryl_analyzer::component_manifest_table::insert(
3143            veryl_parser::resource_table::insert_str(&definition.name),
3144            manifest,
3145        );
3146    }
3147    Ok(())
3148}
3149
3150/// Generate TypeScript type information as JSON for a Veryl project.
3151///
3152/// Equivalent to running `celox-gen-ts --json` from the given project directory.
3153#[napi]
3154pub fn gen_ts(
3155    project_path: String,
3156    components: Option<Vec<NapiInjectedManifest>>,
3157) -> Result<String> {
3158    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
3159
3160    let toml_path = Metadata::search_from(&project_path)
3161        .map_err(|e| Error::from_reason(format!("Could not find Veryl.toml: {e}")))?;
3162    let mut metadata = Metadata::load(&toml_path)
3163        .map_err(|e| Error::from_reason(format!("Failed to load Veryl.toml: {e}")))?;
3164
3165    let base_path = toml_path
3166        .parent()
3167        .unwrap_or(&toml_path)
3168        .to_string_lossy()
3169        .to_string();
3170
3171    let mut paths = metadata
3172        .paths::<std::path::PathBuf>(&[], true, true)
3173        .map_err(|e| Error::from_reason(format!("Failed to gather sources: {e}")))?;
3174    paths.retain(|path| !path.example);
3175
3176    // Append test-only sources declared in celox.toml
3177    let project_root = toml_path.parent().unwrap_or(&toml_path).to_path_buf();
3178    let celox_cfg = load_celox_config(&project_root)?;
3179    let prj_name = metadata.project.name.clone();
3180    for dir in &celox_cfg.test.sources {
3181        let dir_path = project_root.join(dir);
3182        if !dir_path.exists() {
3183            continue;
3184        }
3185        for src in walkdir(&dir_path)? {
3186            paths.push(PathSet {
3187                prj: prj_name.clone(),
3188                src: src.clone(),
3189                dst: src.with_extension("sv"),
3190                map: src.with_extension("map"),
3191                example: false,
3192            });
3193        }
3194    }
3195
3196    if let Some(exclude_set) = build_exclude_set(&celox_cfg)? {
3197        paths.retain(|p| !is_excluded(&p.src, &project_root, &exclude_set));
3198    }
3199
3200    if paths.is_empty() {
3201        return Err(Error::from_reason("No Veryl source files found"));
3202    }
3203
3204    // Parse and analyze pass 1
3205    symbol_table::clear();
3206    attribute_table::clear();
3207
3208    let analyzer = Analyzer::new(&metadata);
3209    inject_analyzer_components(components)?;
3210    let mut parsers = Vec::new();
3211    let mut all_warnings = Vec::new();
3212
3213    for path in &paths {
3214        let input = std::fs::read_to_string(&path.src)
3215            .map_err(|e| Error::from_reason(format!("{}: {e}", path.src.display())))?;
3216        let parser = Parser::parse(&input, &path.src)
3217            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
3218
3219        let results = analyzer.analyze_pass1(&path.prj, &parser.veryl);
3220        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3221        if !real_errors.is_empty() {
3222            return Err(Error::from_reason(format_errors_with_warnings(
3223                "analysis pass 1",
3224                &real_errors,
3225                &all_warnings,
3226            )));
3227        }
3228        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3229
3230        parsers.push((path.clone(), parser));
3231    }
3232
3233    let results = Analyzer::analyze_post_pass1();
3234    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3235    if !real_errors.is_empty() {
3236        return Err(Error::from_reason(format_errors_with_warnings(
3237            "post-pass 1 analysis",
3238            &real_errors,
3239            &all_warnings,
3240        )));
3241    }
3242    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3243
3244    // Pass 2: per-file IR → generate
3245
3246    // Compute all source file relative paths for embedding in generated JS.
3247    let base_normalized = base_path.replace('\\', "/");
3248    let all_source_files: Vec<String> = parsers
3249        .iter()
3250        .map(|(path, _)| {
3251            let src_normalized = path
3252                .src
3253                .to_string_lossy()
3254                .replace(r"\\?\", "")
3255                .replace('\\', "/");
3256            src_normalized
3257                .strip_prefix(&base_normalized)
3258                .unwrap_or(&src_normalized)
3259                .trim_start_matches('/')
3260                .to_string()
3261        })
3262        .collect();
3263    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
3264
3265    let mut all_modules = Vec::new();
3266    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
3267    let mut post_pass_ir = Ir::default();
3268
3269    for (i, (_path, parser)) in parsers.iter().enumerate() {
3270        let mut analyzer_context = Context::default();
3271        let mut ir = Ir::default();
3272        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
3273        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3274        if !real_errors.is_empty() {
3275            return Err(Error::from_reason(format_errors_with_warnings(
3276                "analysis pass 2",
3277                &real_errors,
3278                &all_warnings,
3279            )));
3280        }
3281        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3282
3283        let modules = generate_all(&ir, &source_file_refs);
3284        post_pass_ir.append(&mut ir);
3285        let source_file = all_source_files[i].clone();
3286
3287        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
3288        if !module_names.is_empty() {
3289            file_modules.insert(source_file.clone(), module_names);
3290        }
3291
3292        for m in modules {
3293            all_modules.push(JsonModuleEntry {
3294                module_name: m.module_name,
3295                source_file: source_file.clone(),
3296                dts_content: m.dts_content,
3297                md_content: m.md_content,
3298                ports: m.ports,
3299                events: m.events,
3300                instances: m.instances,
3301                is_test: m.is_test,
3302            });
3303        }
3304    }
3305
3306    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
3307    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3308    if !real_errors.is_empty() {
3309        return Err(Error::from_reason(format_errors_with_warnings(
3310            "post-pass 2 analysis",
3311            &real_errors,
3312            &all_warnings,
3313        )));
3314    }
3315    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3316
3317    // Sort for deterministic output
3318    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
3319
3320    let warning_msgs: Vec<String> = all_warnings
3321        .iter()
3322        .map(|w| celox::render_diagnostic(w))
3323        .collect();
3324
3325    let all_sources: Vec<(String, String)> = parsers
3326        .iter()
3327        .map(|(p, _)| {
3328            let path_str = p.src.to_string_lossy().to_string();
3329            let content = std::fs::read_to_string(&p.src).unwrap_or_default();
3330            (path_str, content)
3331        })
3332        .collect();
3333
3334    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
3335        .iter()
3336        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
3337        .collect();
3338
3339    let output = JsonOutput {
3340        project_path: base_path,
3341        modules: all_modules,
3342        file_modules,
3343        warnings: warning_msgs,
3344        diagnostics,
3345    };
3346
3347    serde_json::to_string(&output)
3348        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
3349}
3350
3351/// Generate TypeScript type information from in-memory Veryl sources.
3352///
3353/// Like `gen_ts()` but does not require a Veryl.toml or filesystem access.
3354/// Works on both native and wasm32 targets.
3355#[napi]
3356pub fn gen_ts_from_source(
3357    sources: Vec<NapiSourceFile>,
3358    components: Option<Vec<NapiInjectedManifest>>,
3359) -> Result<String> {
3360    use celox_ts_gen::{JsonModuleEntry, JsonOutput, generate_all};
3361
3362    if sources.is_empty() {
3363        return Err(Error::from_reason("No source files provided"));
3364    }
3365
3366    let metadata = Metadata::create_default("playground")
3367        .map_err(|e| Error::from_reason(format!("Failed to create default metadata: {e}")))?;
3368
3369    // Parse and analyze pass 1
3370    symbol_table::clear();
3371    attribute_table::clear();
3372
3373    let analyzer = Analyzer::new(&metadata);
3374    inject_analyzer_components(components)?;
3375    let mut parsers = Vec::new();
3376    let mut all_warnings = Vec::new();
3377
3378    for src in &sources {
3379        let path = std::path::PathBuf::from(&src.path);
3380        let parser = Parser::parse(&src.content, &path)
3381            .map_err(|e| Error::from_reason(format!("Parse error: {e}")))?;
3382
3383        let prj = "playground".to_string();
3384        let results = analyzer.analyze_pass1(&prj, &parser.veryl);
3385        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3386        if !real_errors.is_empty() {
3387            return Err(Error::from_reason(format_errors_with_warnings(
3388                "analysis pass 1",
3389                &real_errors,
3390                &all_warnings,
3391            )));
3392        }
3393        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3394
3395        parsers.push((prj, path, parser));
3396    }
3397
3398    let results = Analyzer::analyze_post_pass1();
3399    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3400    if !real_errors.is_empty() {
3401        return Err(Error::from_reason(format_errors_with_warnings(
3402            "post-pass 1 analysis",
3403            &real_errors,
3404            &all_warnings,
3405        )));
3406    }
3407    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3408
3409    // Pass 2: per-file IR → generate
3410    let all_source_files: Vec<String> = parsers
3411        .iter()
3412        .map(|(_, p, _)| p.to_string_lossy().replace('\\', "/"))
3413        .collect();
3414    let source_file_refs: Vec<&str> = all_source_files.iter().map(|s| s.as_str()).collect();
3415
3416    let mut all_modules = Vec::new();
3417    let mut file_modules: HashMap<String, Vec<String>> = HashMap::default();
3418    let mut post_pass_ir = Ir::default();
3419
3420    for (i, (_prj, _path, parser)) in parsers.iter().enumerate() {
3421        let mut analyzer_context = Context::default();
3422        let mut ir = Ir::default();
3423        let results = analyzer.analyze_pass2(&parser.veryl, &mut analyzer_context, Some(&mut ir));
3424        let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3425        if !real_errors.is_empty() {
3426            return Err(Error::from_reason(format_errors_with_warnings(
3427                "analysis pass 2",
3428                &real_errors,
3429                &all_warnings,
3430            )));
3431        }
3432        all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3433
3434        let modules = generate_all(&ir, &source_file_refs);
3435        post_pass_ir.append(&mut ir);
3436        let source_file = all_source_files[i].clone();
3437
3438        let module_names: Vec<String> = modules.iter().map(|m| m.module_name.clone()).collect();
3439        if !module_names.is_empty() {
3440            file_modules.insert(source_file.clone(), module_names);
3441        }
3442
3443        for m in modules {
3444            all_modules.push(JsonModuleEntry {
3445                module_name: m.module_name,
3446                source_file: source_file.clone(),
3447                dts_content: m.dts_content,
3448                md_content: m.md_content,
3449                ports: m.ports,
3450                events: m.events,
3451                instances: m.instances,
3452                is_test: m.is_test,
3453            });
3454        }
3455    }
3456
3457    let results = Analyzer::analyze_post_pass2(&post_pass_ir);
3458    let real_errors: Vec<_> = results.iter().filter(|e| e.is_error()).collect();
3459    if !real_errors.is_empty() {
3460        return Err(Error::from_reason(format_errors_with_warnings(
3461            "post-pass 2 analysis",
3462            &real_errors,
3463            &all_warnings,
3464        )));
3465    }
3466    all_warnings.extend(results.into_iter().filter(|e| !e.is_error()));
3467
3468    // Sort for deterministic output
3469    all_modules.sort_by(|a, b| a.module_name.cmp(&b.module_name));
3470
3471    let warning_msgs: Vec<String> = all_warnings
3472        .iter()
3473        .map(|w| celox::render_diagnostic(w))
3474        .collect();
3475
3476    let all_sources: Vec<(String, String)> = sources
3477        .iter()
3478        .map(|s| (s.path.clone(), s.content.clone()))
3479        .collect();
3480
3481    let diagnostics: Vec<celox_ts_gen::JsonDiagnostic> = all_warnings
3482        .iter()
3483        .flat_map(|w| analyzer_error_to_diagnostics(w, &all_sources, false))
3484        .collect();
3485
3486    let output = JsonOutput {
3487        project_path: String::new(),
3488        modules: all_modules,
3489        file_modules,
3490        warnings: warning_msgs,
3491        diagnostics,
3492    };
3493
3494    serde_json::to_string(&output)
3495        .map_err(|e| Error::from_reason(format!("Failed to serialize JSON: {e}")))
3496}
3497
3498#[cfg(all(test, not(target_arch = "wasm32")))]
3499mod tests {
3500    use super::*;
3501
3502    fn default_opts() -> ParsedOptions {
3503        ParsedOptions {
3504            common: ParsedOptionsCommon {
3505                four_state: false,
3506                optimize_options: celox::OptimizeOptions::all(),
3507                vcd: None,
3508                false_loops: vec![],
3509                true_loops: vec![],
3510                clock_type: None,
3511                reset_type: None,
3512                extra_source: None,
3513                parameters: vec![],
3514            },
3515            cranelift_options: celox::CraneliftOptions::default(),
3516            dead_store_policy: celox::DeadStorePolicy::Off,
3517        }
3518    }
3519
3520    fn make_sources(pairs: &[(&str, &str)]) -> Vec<(String, std::path::PathBuf)> {
3521        pairs
3522            .iter()
3523            .map(|(content, path)| (content.to_string(), std::path::PathBuf::from(path)))
3524            .collect()
3525    }
3526
3527    fn napi_sources(source: &str) -> Vec<NapiSourceFile> {
3528        vec![NapiSourceFile {
3529            content: source.to_string(),
3530            path: "top.veryl".to_string(),
3531        }]
3532    }
3533
3534    const NO_EVENTS_SOURCE: &str = "module Top () {}";
3535    const TWO_EVENTS_SOURCE: &str = r#"
3536        module Top (
3537            clk: input clock,
3538            rst: input reset,
3539            en: input logic,
3540            count: output logic<8>,
3541        ) {
3542            var count_r: logic<8>;
3543
3544            always_ff (clk, rst) {
3545                if_reset {
3546                    count_r = 0;
3547                } else if en {
3548                    count_r = count_r + 1;
3549                }
3550            }
3551
3552            always_comb {
3553                count = count_r;
3554            }
3555        }
3556    "#;
3557
3558    #[test]
3559    fn raw_event_methods_reject_eventless_handles_with_the_same_error() {
3560        let mut simulator =
3561            NativeSimulatorHandle::new(napi_sources(NO_EVENTS_SOURCE), "Top".into(), None).unwrap();
3562        let mut simulation =
3563            NativeSimulationHandle::new(napi_sources(NO_EVENTS_SOURCE), "Top".into(), None)
3564                .unwrap();
3565
3566        let errors = [
3567            simulator.tick(0).unwrap_err(),
3568            simulator.tick_n(0, 0).unwrap_err(),
3569            simulation.add_clock(0, 10.0, 0.0).unwrap_err(),
3570            simulation.schedule(0, 0.0, 1.0).unwrap_err(),
3571        ];
3572        let expected = celox::RuntimeErrorCode::NotAnEvent("event_id=0".to_string()).to_string();
3573
3574        for error in errors {
3575            assert_eq!(error.reason, expected);
3576        }
3577    }
3578
3579    #[test]
3580    fn raw_event_methods_reject_the_first_out_of_bounds_id() {
3581        let mut simulator =
3582            NativeSimulatorHandle::new(napi_sources(TWO_EVENTS_SOURCE), "Top".into(), None)
3583                .unwrap();
3584        let mut simulation =
3585            NativeSimulationHandle::new(napi_sources(TWO_EVENTS_SOURCE), "Top".into(), None)
3586                .unwrap();
3587
3588        let errors = [
3589            simulator.tick(2).unwrap_err(),
3590            simulator.tick_n(2, 0).unwrap_err(),
3591            simulation.add_clock(2, 10.0, 0.0).unwrap_err(),
3592            simulation.schedule(2, 0.0, 1.0).unwrap_err(),
3593        ];
3594        let expected = celox::RuntimeErrorCode::NotAnEvent("event_id=2".to_string()).to_string();
3595
3596        for error in errors {
3597            assert_eq!(error.reason, expected);
3598        }
3599    }
3600
3601    #[test]
3602    fn disposed_errors_take_precedence_over_event_validation() {
3603        let mut simulator =
3604            NativeSimulatorHandle::new(napi_sources(NO_EVENTS_SOURCE), "Top".into(), None).unwrap();
3605        simulator.dispose().unwrap();
3606        assert_eq!(
3607            simulator.tick(0).unwrap_err().reason,
3608            "Simulator has been disposed"
3609        );
3610        assert_eq!(
3611            simulator.tick_n(0, 0).unwrap_err().reason,
3612            "Simulator has been disposed"
3613        );
3614
3615        let mut simulation =
3616            NativeSimulationHandle::new(napi_sources(NO_EVENTS_SOURCE), "Top".into(), None)
3617                .unwrap();
3618        simulation.dispose().unwrap();
3619        assert_eq!(
3620            simulation.add_clock(0, 10.0, 0.0).unwrap_err().reason,
3621            "Simulation has been disposed"
3622        );
3623        assert_eq!(
3624            simulation.schedule(0, 0.0, 1.0).unwrap_err().reason,
3625            "Simulation has been disposed"
3626        );
3627    }
3628
3629    #[test]
3630    fn native_handle_dumps_consume_activity_until_memory_is_exposed() {
3631        let source = "module Top (
3632            clk: input clock, q: output logic<64>,
3633            a: input logic<512>, b: input logic<512>, c: input logic<512>,
3634        ) { always_ff (clk) { q += 1; } }";
3635        let dir = tempfile::tempdir().unwrap();
3636        // Warm the uninstrumented cache first. A traced build must not reuse it.
3637        NativeSimulatorHandle::new(napi_sources(source), "Top".into(), None)
3638            .unwrap()
3639            .dispose()
3640            .unwrap();
3641        for mode in ["fresh", "cached", "tiered"] {
3642            let path = dir.path().join(format!("{mode}.vcd"));
3643            let options = Some(NapiOptions {
3644                vcd: Some(path.to_str().unwrap().to_owned()),
3645                four_state: None,
3646                opt_level: None,
3647                pass_overrides: None,
3648                optimize: None,
3649                optimize_options: None,
3650                cranelift_opt_level: None,
3651                regalloc_algorithm: None,
3652                enable_alias_analysis: None,
3653                enable_verifier: None,
3654                false_loops: None,
3655                true_loops: None,
3656                clock_type: None,
3657                reset_type: None,
3658                extra_source: None,
3659                parameters: None,
3660                dead_store_policy: None,
3661                threads: None,
3662            });
3663            let mut handle = if mode == "tiered" {
3664                NativeSimulatorHandle::new_tiered(napi_sources(source), "Top".into(), options)
3665            } else {
3666                NativeSimulatorHandle::new(napi_sources(source), "Top".into(), options)
3667            }
3668            .unwrap();
3669            handle.dump(0.0).unwrap();
3670            let initial = handle.vcd_writer.as_ref().unwrap().statistics();
3671            assert!(initial.comparisons > 0);
3672            handle.dump(1.0).unwrap();
3673            assert_eq!(
3674                handle.vcd_writer.as_ref().unwrap().statistics().comparisons,
3675                initial.comparisons,
3676                "{mode}: idle dumps must not rescan memory"
3677            );
3678            handle.tick(0).unwrap();
3679            handle.dump(2.0).unwrap();
3680            let changed = handle.vcd_writer.as_ref().unwrap().statistics();
3681            assert!(changed.changes > initial.changes);
3682            assert!(changed.comparisons - initial.comparisons < initial.comparisons);
3683            handle.dump(3.0).unwrap();
3684            assert_eq!(
3685                handle.vcd_writer.as_ref().unwrap().statistics().comparisons,
3686                changed.comparisons
3687            );
3688            // shared_memory() obtains its pointer through this same dispatch.
3689            handle.backend.as_mut().unwrap().memory_as_mut_ptr();
3690            handle.dump(4.0).unwrap();
3691            assert_eq!(
3692                handle.vcd_writer.as_ref().unwrap().statistics().comparisons,
3693                changed.comparisons + initial.comparisons
3694            );
3695            handle.dispose().unwrap();
3696            assert!(std::fs::read_to_string(path).unwrap().contains("\nb1 "));
3697        }
3698    }
3699
3700    #[test]
3701    fn standard_event_metadata_is_dense_and_valid_ids_still_work() {
3702        let mut simulator =
3703            NativeSimulatorHandle::new(napi_sources(TWO_EVENTS_SOURCE), "Top".into(), None)
3704                .unwrap();
3705        let simulator_events: HashMap<String, u32> =
3706            serde_json::from_str(&simulator.events_json()).unwrap();
3707        let mut simulator_ids = simulator_events.values().copied().collect::<Vec<_>>();
3708        simulator_ids.sort_unstable();
3709        assert_eq!(simulator_ids, [0, 1]);
3710        assert_eq!(
3711            simulator.backend.as_ref().unwrap().event_count(),
3712            simulator_ids.len()
3713        );
3714        simulator.tick(0).unwrap();
3715        simulator.tick_n(1, 0).unwrap();
3716
3717        let mut simulation =
3718            NativeSimulationHandle::new(napi_sources(TWO_EVENTS_SOURCE), "Top".into(), None)
3719                .unwrap();
3720        let simulation_events: HashMap<String, u32> =
3721            serde_json::from_str(&simulation.events_json()).unwrap();
3722        let mut simulation_ids = simulation_events.values().copied().collect::<Vec<_>>();
3723        simulation_ids.sort_unstable();
3724        assert_eq!(simulation_ids, [0, 1]);
3725        simulation.add_clock(0, 10.0, 0.0).unwrap();
3726        simulation.schedule(1, 5.0, 1.0).unwrap();
3727    }
3728
3729    #[test]
3730    fn simulator_handle_derives_four_state_metadata_from_layout() {
3731        let source = "module Top (a: input logic<1>) {}";
3732        let path = std::path::Path::new("top.veryl");
3733
3734        for four_state in [false, true] {
3735            let simulator = celox::Simulator::from_sources(vec![(source, path)], "Top")
3736                .four_state(four_state)
3737                .build()
3738                .unwrap();
3739            let handle = NativeSimulatorHandle::from_simulator(simulator, None).unwrap();
3740            let layout: serde_json::Value = serde_json::from_str(&handle.layout_json()).unwrap();
3741
3742            assert_eq!(layout["a"]["is_4state"].as_bool(), Some(four_state));
3743        }
3744    }
3745
3746    #[test]
3747    fn normal_project_loading_excludes_example_sources() {
3748        let project = tempfile::tempdir().unwrap();
3749        std::fs::create_dir(project.path().join("src")).unwrap();
3750        std::fs::create_dir(project.path().join("examples")).unwrap();
3751        std::fs::write(
3752            project.path().join("Veryl.toml"),
3753            "[project]\nname = \"example_filter\"\nversion = \"0.1.0\"\n",
3754        )
3755        .unwrap();
3756        std::fs::write(project.path().join("src/top.veryl"), "module Top () {}\n").unwrap();
3757        std::fs::write(
3758            project.path().join("examples/demo.veryl"),
3759            "module Demo () {}\n",
3760        )
3761        .unwrap();
3762
3763        let (sources, _, _) = load_project_sources(project.path().to_str().unwrap()).unwrap();
3764        let source_names = sources
3765            .iter()
3766            .map(|(_, path)| path.file_name().unwrap().to_str().unwrap())
3767            .collect::<Vec<_>>();
3768        assert_eq!(source_names, ["top.veryl"]);
3769    }
3770
3771    #[test]
3772    fn same_inputs_produce_same_key() {
3773        let src = make_sources(&[("module Top {}", "a.veryl")]);
3774        let opts = default_opts();
3775        let k1 = build_cache_key(&src, "Top", &opts, None);
3776        let k2 = build_cache_key(&src, "Top", &opts, None);
3777        assert_eq!(k1, k2);
3778    }
3779
3780    #[test]
3781    fn high_index_sir_pass_changes_cache_key_without_bit_packing() {
3782        let src = make_sources(&[("module Top {}", "a.veryl")]);
3783        let enabled = default_opts();
3784        let mut disabled = default_opts();
3785        disabled.common.optimize_options = disabled
3786            .common
3787            .optimize_options
3788            .clone()
3789            .disable(celox::SirPass::IdentityStoreBypass);
3790
3791        assert_ne!(
3792            build_cache_key(&src, "Top", &enabled, None),
3793            build_cache_key(&src, "Top", &disabled, None)
3794        );
3795    }
3796
3797    #[test]
3798    fn native_memory_width_changes_cache_key() {
3799        let src = make_sources(&[("module Top {}", "a.veryl")]);
3800        let mut narrow = default_opts();
3801        narrow.common.optimize_options = narrow
3802            .common
3803            .optimize_options
3804            .clone()
3805            .with_max_native_memory_width(64);
3806        let mut wide = default_opts();
3807        wide.common.optimize_options = wide
3808            .common
3809            .optimize_options
3810            .clone()
3811            .with_max_native_memory_width(128);
3812
3813        assert_ne!(
3814            build_cache_key(&src, "Top", &narrow, None),
3815            build_cache_key(&src, "Top", &wide, None)
3816        );
3817    }
3818
3819    #[test]
3820    fn different_source_content_different_key() {
3821        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3822        let s2 = make_sources(&[("module B {}", "a.veryl")]);
3823        let opts = default_opts();
3824        assert_ne!(
3825            build_cache_key(&s1, "Top", &opts, None),
3826            build_cache_key(&s2, "Top", &opts, None),
3827        );
3828    }
3829
3830    #[test]
3831    fn different_source_path_different_key() {
3832        let s1 = make_sources(&[("module A {}", "a.veryl")]);
3833        let s2 = make_sources(&[("module A {}", "b.veryl")]);
3834        let opts = default_opts();
3835        assert_ne!(
3836            build_cache_key(&s1, "Top", &opts, None),
3837            build_cache_key(&s2, "Top", &opts, None),
3838        );
3839    }
3840
3841    #[test]
3842    fn different_top_different_key() {
3843        let src = make_sources(&[("module Top {}", "a.veryl")]);
3844        let opts = default_opts();
3845        assert_ne!(
3846            build_cache_key(&src, "Top", &opts, None),
3847            build_cache_key(&src, "Other", &opts, None),
3848        );
3849    }
3850
3851    #[test]
3852    fn four_state_differs() {
3853        let src = make_sources(&[("module Top {}", "a.veryl")]);
3854        let mut o1 = default_opts();
3855        let mut o2 = default_opts();
3856        o1.common.four_state = false;
3857        o2.common.four_state = true;
3858        assert_ne!(
3859            build_cache_key(&src, "Top", &o1, None),
3860            build_cache_key(&src, "Top", &o2, None),
3861        );
3862    }
3863
3864    #[test]
3865    fn optimize_options_differs() {
3866        let src = make_sources(&[("module Top {}", "a.veryl")]);
3867        let mut o1 = default_opts();
3868        let mut o2 = default_opts();
3869        o1.common.optimize_options = celox::OptimizeOptions::all();
3870        o2.common.optimize_options = celox::OptimizeOptions::none();
3871        assert_ne!(
3872            build_cache_key(&src, "Top", &o1, None),
3873            build_cache_key(&src, "Top", &o2, None),
3874        );
3875    }
3876
3877    #[test]
3878    fn cranelift_opt_level_differs() {
3879        let src = make_sources(&[("module Top {}", "a.veryl")]);
3880        let mut o1 = default_opts();
3881        let mut o2 = default_opts();
3882        o1.cranelift_options.opt_level = celox::CraneliftOptLevel::Speed;
3883        o2.cranelift_options.opt_level = celox::CraneliftOptLevel::None;
3884        assert_ne!(
3885            build_cache_key(&src, "Top", &o1, None),
3886            build_cache_key(&src, "Top", &o2, None),
3887        );
3888    }
3889
3890    #[test]
3891    fn regalloc_algorithm_differs() {
3892        let src = make_sources(&[("module Top {}", "a.veryl")]);
3893        let mut o1 = default_opts();
3894        let mut o2 = default_opts();
3895        o1.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::Backtracking;
3896        o2.cranelift_options.regalloc_algorithm = celox::RegallocAlgorithm::SinglePass;
3897        assert_ne!(
3898            build_cache_key(&src, "Top", &o1, None),
3899            build_cache_key(&src, "Top", &o2, None),
3900        );
3901    }
3902
3903    #[test]
3904    fn dead_store_policy_differs() {
3905        let src = make_sources(&[("module Top {}", "a.veryl")]);
3906        let mut o1 = default_opts();
3907        let mut o2 = default_opts();
3908        o1.dead_store_policy = celox::DeadStorePolicy::Off;
3909        o2.dead_store_policy = celox::DeadStorePolicy::PreserveTopPorts;
3910        assert_ne!(
3911            build_cache_key(&src, "Top", &o1, None),
3912            build_cache_key(&src, "Top", &o2, None),
3913        );
3914    }
3915
3916    #[test]
3917    fn clock_type_differs() {
3918        let src = make_sources(&[("module Top {}", "a.veryl")]);
3919        let mut o1 = default_opts();
3920        let mut o2 = default_opts();
3921        o1.common.clock_type = None;
3922        o2.common.clock_type = Some(celox::ClockType::NegEdge);
3923        assert_ne!(
3924            build_cache_key(&src, "Top", &o1, None),
3925            build_cache_key(&src, "Top", &o2, None),
3926        );
3927    }
3928
3929    #[test]
3930    fn reset_type_differs() {
3931        let src = make_sources(&[("module Top {}", "a.veryl")]);
3932        let mut o1 = default_opts();
3933        let mut o2 = default_opts();
3934        o1.common.reset_type = None;
3935        o2.common.reset_type = Some(celox::ResetType::SyncHigh);
3936        assert_ne!(
3937            build_cache_key(&src, "Top", &o1, None),
3938            build_cache_key(&src, "Top", &o2, None),
3939        );
3940    }
3941
3942    #[test]
3943    fn parameters_differ() {
3944        let src = make_sources(&[("module Top {}", "a.veryl")]);
3945        let mut o1 = default_opts();
3946        let mut o2 = default_opts();
3947        o1.common.parameters = vec![("WIDTH".into(), 8)];
3948        o2.common.parameters = vec![("WIDTH".into(), 16)];
3949        assert_ne!(
3950            build_cache_key(&src, "Top", &o1, None),
3951            build_cache_key(&src, "Top", &o2, None),
3952        );
3953    }
3954
3955    #[test]
3956    fn source_order_independent() {
3957        let s1 = make_sources(&[("aaa", "a.veryl"), ("bbb", "b.veryl")]);
3958        let s2 = make_sources(&[("bbb", "b.veryl"), ("aaa", "a.veryl")]);
3959        let opts = default_opts();
3960        assert_eq!(
3961            build_cache_key(&s1, "Top", &opts, None),
3962            build_cache_key(&s2, "Top", &opts, None),
3963        );
3964    }
3965
3966    #[test]
3967    fn vcd_paths_share_compiled_code_but_tracing_modes_do_not() {
3968        let src = make_sources(&[("module Top {}", "a.veryl")]);
3969        let mut o1 = default_opts();
3970        let mut o2 = default_opts();
3971        // VCD instrumentation affects compilation, but its destination does not.
3972        o1.common.vcd = None;
3973        o2.common.vcd = Some("/tmp/dump.vcd".into());
3974        assert_ne!(
3975            build_cache_key(&src, "Top", &o1, None),
3976            build_cache_key(&src, "Top", &o2, None),
3977        );
3978        o1.common.vcd = Some("/tmp/another.vcd".into());
3979        assert_eq!(
3980            build_cache_key(&src, "Top", &o1, None),
3981            build_cache_key(&src, "Top", &o2, None),
3982        );
3983    }
3984
3985    #[test]
3986    fn false_loops_differ() {
3987        let src = make_sources(&[("module Top {}", "a.veryl")]);
3988        let mut o1 = default_opts();
3989        let mut o2 = default_opts();
3990        o1.common.false_loops = vec![];
3991        o2.common.false_loops = vec![((vec![], vec!["a".into()]), (vec![], vec!["b".into()]))];
3992        assert_ne!(
3993            build_cache_key(&src, "Top", &o1, None),
3994            build_cache_key(&src, "Top", &o2, None),
3995        );
3996    }
3997
3998    #[test]
3999    fn true_loops_differ() {
4000        let src = make_sources(&[("module Top {}", "a.veryl")]);
4001        let mut o1 = default_opts();
4002        let mut o2 = default_opts();
4003        o1.common.true_loops = vec![];
4004        o2.common.true_loops = vec![((vec![], vec!["x".into()]), (vec![], vec!["y".into()]), 4)];
4005        assert_ne!(
4006            build_cache_key(&src, "Top", &o1, None),
4007            build_cache_key(&src, "Top", &o2, None),
4008        );
4009    }
4010
4011    #[test]
4012    fn metadata_clock_reset_differs() {
4013        let src = make_sources(&[("module Top {}", "a.veryl")]);
4014        let opts = default_opts();
4015
4016        let mut m1 = Metadata::create_default("prj").unwrap();
4017        let mut m2 = Metadata::create_default("prj").unwrap();
4018        m1.build.clock_type = celox::ClockType::PosEdge;
4019        m2.build.clock_type = celox::ClockType::NegEdge;
4020        assert_ne!(
4021            build_cache_key(&src, "Top", &opts, Some(&m1)),
4022            build_cache_key(&src, "Top", &opts, Some(&m2)),
4023        );
4024
4025        let mut m3 = Metadata::create_default("prj").unwrap();
4026        let mut m4 = Metadata::create_default("prj").unwrap();
4027        m3.build.reset_type = celox::ResetType::AsyncLow;
4028        m4.build.reset_type = celox::ResetType::SyncHigh;
4029        assert_ne!(
4030            build_cache_key(&src, "Top", &opts, Some(&m3)),
4031            build_cache_key(&src, "Top", &opts, Some(&m4)),
4032        );
4033    }
4034
4035    #[test]
4036    fn no_metadata_vs_metadata_differs() {
4037        let src = make_sources(&[("module Top {}", "a.veryl")]);
4038        let opts = default_opts();
4039        let m = Metadata::create_default("prj").unwrap();
4040        // No metadata vs with metadata should differ (metadata adds clock/reset info)
4041        assert_ne!(
4042            build_cache_key(&src, "Top", &opts, None),
4043            build_cache_key(&src, "Top", &opts, Some(&m)),
4044        );
4045    }
4046}