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