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