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