Skip to main content

celox_napi/
lib.rs

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