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