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