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