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