use crate::HashSet;
use veryl_parser::resource_table::{self, StrId};
pub(crate) use celox_frontend_veryl::BuildConfig;
pub(crate) mod loop_provenance;
use crate::ir::{RegionedAbsoluteAddr, RuntimeProgram, STABLE_REGION, SirProgram, UnoptimizedSir};
pub use celox_frontend_veryl::ParserError;
#[cfg(test)]
pub use celox_frontend_veryl::parse_ir;
use celox_frontend_veryl::parse_ir_with_loop_provenance;
fn apply_fused_optimization_hints(
scheduled: &mut celox_frontend_core::ScheduledRtl,
hints: celox_frontend_core::FusedSirOptimizationHints,
) -> Result<(), ParserError> {
for (event, direct_ff_writes) in hints.direct_ff_writes {
let Some(units) = scheduled.sir.eval_comb_apply_ffs.get_mut(&event) else {
return Err(ParserError::illegal_context(
"fused comb/FF optimization hints",
format!("event {event} has hints but no scheduled SIR"),
None,
));
};
for unit in units {
let removed =
crate::optimizer::sir::eliminate_shared_comb_state_stores(unit, &direct_ff_writes)
.map_err(|error| {
ParserError::illegal_context(
"shared comb/FF state-publication DSE",
error.to_string(),
None,
)
})?;
if removed != 0 {
crate::optimizer::sir::remove_dead_sir_definitions(unit);
}
if crate::optimizer::sir::promote_fused_comb_static_slots(unit).map_err(|error| {
ParserError::illegal_context(
"fused comb StateSSA promotion",
error.to_string(),
None,
)
})? {
crate::optimizer::sir::remove_dead_sir_definitions(unit);
}
}
}
Ok(())
}
fn dump_addr_map_if_requested(program: &RuntimeProgram, diagnostics: &crate::RuntimeDiagnostics) {
let Some(raw_filter) = diagnostics.address_map_filter.as_deref() else {
return;
};
let filter = parse_addr_map_filter(raw_filter);
let mut entries = Vec::new();
for instance in program.design.instances() {
for address in instance.state_addresses() {
let variable = program.design.variable(address).unwrap();
let inst_key = instance.id.0.to_string();
let var_key = normalized_addr_id(&variable.source_id.to_string());
if let Some(filter) = &filter
&& !filter.contains(&(inst_key, var_key))
{
continue;
}
entries.push((instance, variable));
}
}
entries.sort_by(|(a_instance, a_variable), (b_instance, b_variable)| {
(a_instance.id.0, a_variable.source_id).cmp(&(b_instance.id.0, b_variable.source_id))
});
for (instance, variable) in entries {
let metadata = &program.design.state_objects[&variable.address];
tracing::debug!(
"[addr-map] inst={} var={} module={} path={} width={} array_dims={:?} 4state={} kind={:?} var_kind={}",
instance.id,
variable.source_id,
instance.module_name,
program.get_path(&variable.address),
metadata.width,
metadata.array_dims,
metadata.is_4state,
metadata.kind,
variable.var_kind.description(),
);
}
}
fn parse_addr_map_filter(raw: &str) -> Option<HashSet<(String, String)>> {
if raw.is_empty() {
return None;
}
let mut filter = HashSet::default();
for item in raw
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
{
let Some((inst, var)) = item.split_once(':') else {
continue;
};
filter.insert((normalized_addr_id(inst), normalized_addr_id(var)));
}
(!filter.is_empty()).then_some(filter)
}
fn normalized_addr_id(raw: &str) -> String {
raw.trim()
.trim_start_matches("inst")
.trim_start_matches("var")
.to_string()
}
fn verify_program_sir(
sir: &SirProgram,
program: &RuntimeProgram,
phase: &'static str,
) -> Result<(), ParserError> {
let units = sir
.eval_comb
.iter()
.enumerate()
.map(|(unit, eu)| ("eval_comb", unit, eu))
.chain(
sir.eval_apply_ffs
.values()
.flatten()
.enumerate()
.map(|(unit, eu)| ("eval_apply_ffs", unit, eu)),
)
.chain(
sir.eval_comb_apply_ffs
.values()
.flatten()
.enumerate()
.map(|(unit, eu)| ("eval_comb_apply_ffs", unit, eu)),
)
.chain(
sir.eval_only_ffs
.values()
.flatten()
.enumerate()
.map(|(unit, eu)| ("eval_only_ffs", unit, eu)),
)
.chain(
sir.apply_ffs
.values()
.flatten()
.enumerate()
.map(|(unit, eu)| ("apply_ffs", unit, eu)),
);
for (group, unit, eu) in units {
verify_memory_offset_contract(program, eu).map_err(|error| ParserError::SirVerify {
phase,
group,
unit,
error,
})?;
verify_region_contract(group, eu).map_err(|error| ParserError::SirVerify {
phase,
group,
unit,
error,
})?;
eu.verify_result().map_err(|error| ParserError::SirVerify {
phase,
group,
unit,
error,
})?;
}
Ok(())
}
pub(crate) fn verify_memory_offset_contract(
program: &RuntimeProgram,
eu: &crate::ir::ExecutionUnit<RegionedAbsoluteAddr>,
) -> Result<(), crate::ir::verify::SirVerifyError> {
celox_sir_opt::verify_memory_offset_contract(&program.design, eu)
}
fn verify_region_contract(
group: &'static str,
eu: &crate::ir::ExecutionUnit<RegionedAbsoluteAddr>,
) -> Result<(), crate::ir::verify::SirVerifyError> {
use crate::ir::{SIRInstruction, SIROffset, SPARSE_WORKING_REGION};
for block in eu.blocks.values() {
for (index, inst) in block.instructions.iter().enumerate() {
match inst {
SIRInstruction::Load(_, addr, _, _)
| SIRInstruction::Store(addr, _, _, _, _, _)
if addr.region > SPARSE_WORKING_REGION =>
{
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.KNOWN_MEMORY_REGION",
block.id,
index,
format!("unknown memory region {}", addr.region),
));
}
SIRInstruction::Commit(src, dst, _, _, _)
if src.region > SPARSE_WORKING_REGION || dst.region > SPARSE_WORKING_REGION =>
{
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.KNOWN_MEMORY_REGION",
block.id,
index,
format!("unknown Commit region pair {}→{}", src.region, dst.region),
));
}
SIRInstruction::Load(_, addr, _, _) if addr.region == SPARSE_WORKING_REGION => {
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.SPARSE_IS_NOT_READABLE",
block.id,
index,
"FF sparse next-state storage cannot be loaded; FF RHS values read STABLE",
));
}
SIRInstruction::Store(addr, _, _, _, _, _)
if addr.region == SPARSE_WORKING_REGION
&& !matches!(
group,
"eval_only_ffs" | "eval_apply_ffs" | "eval_comb_apply_ffs"
) =>
{
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.SPARSE_STORE_IN_EVALUATOR",
block.id,
index,
format!("SPARSE Store is not valid in {group}"),
));
}
SIRInstruction::Commit(src, dst, offset, _, triggers)
if src.region == SPARSE_WORKING_REGION =>
{
if dst.region != STABLE_REGION
|| !matches!(
group,
"apply_ffs" | "eval_apply_ffs" | "eval_comb_apply_ffs"
)
|| !matches!(offset, SIROffset::Static(0))
|| !triggers.is_empty()
{
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.SPARSE_COMMIT_FORM",
block.id,
index,
"SPARSE Commit must be an untriggered full-range SPARSE→STABLE apply",
));
}
}
SIRInstruction::Commit(_, dst, _, _, _) if dst.region == SPARSE_WORKING_REGION => {
return Err(crate::ir::verify::SirVerifyError::instruction(
"REGION.SPARSE_IS_NOT_COMMIT_DESTINATION",
block.id,
index,
"SPARSE is populated by Store, not by Commit",
));
}
_ => {}
}
}
}
Ok(())
}
pub(crate) fn finalize_scheduled_rtl(
mut scheduled: celox_frontend_core::ScheduledRtlOutput,
mut testbench_source: Option<celox_frontend_veryl::VerylTestbenchSource>,
four_state: bool,
trace_opts: &crate::debug::TraceOptions,
mut trace: Option<&mut crate::debug::CompilationTrace>,
optimize_options: &crate::optimizer::OptimizeOptions,
diagnostics: &crate::RuntimeDiagnostics,
preserve_element_storage_layout: bool,
testbench_random_seed: Option<u64>,
component_libraries: Vec<celox_testbench::ComponentLibrary>,
component_file_base: Option<std::path::PathBuf>,
) -> Result<crate::ir::OptimizedSir, ParserError> {
let phase_timing = diagnostics.phase_timing;
macro_rules! timed_phase {
($label:expr, $body:expr) => {{
if phase_timing {
let start = crate::timing::now();
let result = $body;
tracing::debug!("[phase-timing] {}: {:?}", $label, start.elapsed());
result
} else {
$body
}
}};
}
apply_fused_optimization_hints(&mut scheduled.scheduled, scheduled.fused_optimization_hints)?;
scheduled.scheduled.inject_triggers();
let testbench = if let Some(testbench_source) = testbench_source.as_mut() {
testbench_source.component_libraries = component_libraries;
testbench_source.component_file_base = component_file_base;
crate::testbench_compile::project_observability(
&scheduled.scheduled.frontend_lookup,
&mut scheduled.scheduled.runtime_schema,
testbench_source,
)?;
crate::testbench_compile::compile_semantic_testbench(
&scheduled.scheduled.frontend_lookup,
scheduled.scheduled.runtime_schema.runtime_event_sites.len(),
testbench_source,
testbench_random_seed,
)?
} else {
None
};
let (sir, mut runtime) =
RuntimeProgram::from_scheduled(scheduled.scheduled).map_err(|error| {
ParserError::illegal_context("runtime design projection", error.to_string(), None)
})?;
runtime.testbench = testbench;
dump_addr_map_if_requested(&runtime, diagnostics);
let mut program = UnoptimizedSir::new(sir, runtime);
if let Some(t) = trace.as_deref_mut()
&& trace_opts.pre_optimized_sir
{
t.pre_optimized_sir = Some(program.clone());
}
let verify_boundaries =
cfg!(debug_assertions) || optimize_options.diagnostics.verify_boundaries;
if verify_boundaries {
timed_phase!(
"verify_sir_before_optimize",
verify_program_sir(&program.sir, &program.runtime, "before optimization")
)?;
}
timed_phase!("optimize", {
if preserve_element_storage_layout {
crate::optimizer::optimize_preserving_element_storage(
&mut program,
four_state,
optimize_options,
)
} else {
crate::optimizer::optimize(&mut program, four_state, optimize_options)
}
});
if verify_boundaries {
timed_phase!(
"verify_sir_after_optimize",
verify_program_sir(&program.sir, &program.runtime, "after optimization")
)?;
}
let program = program.into_optimized();
if let Some(t) = trace
&& trace_opts.post_optimized_sir
{
t.post_optimized_sir = Some(program.clone());
}
Ok(program)
}
fn dynamic_for_diagnostics(
scheduled: &celox_frontend_veryl::VerylScheduledRtlOutput,
ir: &veryl_analyzer::ir::Ir,
) -> Vec<celox_frontend_veryl::FrontendDiagnostic> {
scheduled
.scheduled
.frontend_lookup
.root_instance_and_module()
.and_then(|(_, module)| {
scheduled
.scheduled
.frontend_lookup
.module_names
.get(&module)
.cloned()
})
.and_then(|root_module_name| {
ir.components.iter().find_map(|component| match component {
veryl_analyzer::ir::Component::Module(module)
if resource_table::get_str_value(module.name).as_deref()
== Some(root_module_name.as_str()) =>
{
Some(module)
}
_ => None,
})
})
.map(|module| {
celox_frontend_veryl::check_elaborated_dynamic_for_bounds(
&scheduled.scheduled,
&scheduled.testbench_source,
module,
&scheduled.fused_optimization_hints,
)
})
.unwrap_or_default()
}
pub fn parse(
top: &StrId,
ir: &veryl_analyzer::ir::Ir,
loop_provenance: &loop_provenance::LoopProvenance,
config: &BuildConfig,
ignored_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
)],
true_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
usize,
)],
four_state: bool,
trace_opts: &crate::debug::TraceOptions,
mut trace: Option<&mut crate::debug::CompilationTrace>,
optimize_options: &crate::optimizer::OptimizeOptions,
diagnostics: &crate::RuntimeDiagnostics,
preserve_element_storage_layout: bool,
testbench_random_seed: Option<u64>,
component_libraries: Vec<celox_testbench::ComponentLibrary>,
component_file_base: Option<std::path::PathBuf>,
) -> Result<
(
crate::ir::OptimizedSir,
Vec<celox_frontend_veryl::FrontendDiagnostic>,
),
ParserError,
> {
debug_assert!(
loop_provenance.is_consistent_with(ir),
"loop provenance must describe the analyzer IR passed to the parser"
);
let phase_timing = diagnostics.phase_timing;
macro_rules! timed_phase {
($label:expr, $body:expr) => {{
if phase_timing {
let start = crate::timing::now();
let result = $body;
tracing::debug!("[phase-timing] {}: {:?}", $label, start.elapsed());
result
} else {
$body
}
}};
}
let result = timed_phase!(
"parse_ir",
parse_ir_with_loop_provenance(ir, loop_provenance, config, top)
)?;
if let Some(t) = trace.as_deref_mut()
&& trace_opts.analyzer_ir
{
t.analyzer_ir = Some(ir.to_string());
}
let frontend_trace_options = trace_opts.frontend(diagnostics);
let mut frontend_trace = celox_frontend_core::FrontendTrace::default();
let scheduled = timed_phase!(
"flatten",
celox_frontend_veryl::schedule_symbolic_rtl(
result,
config,
ignored_loops,
true_loops,
four_state,
&frontend_trace_options,
trace.is_some().then_some(&mut frontend_trace),
)
);
if let Some(trace) = trace.as_deref_mut() {
trace.absorb_frontend(frontend_trace);
}
let scheduled = scheduled?;
let dynamic_for_diagnostics = dynamic_for_diagnostics(&scheduled, ir);
let celox_frontend_veryl::VerylScheduledRtlOutput {
scheduled,
fused_optimization_hints,
testbench_source,
} = scheduled;
let program = finalize_scheduled_rtl(
celox_frontend_core::ScheduledRtlOutput {
scheduled,
fused_optimization_hints,
},
Some(testbench_source),
four_state,
trace_opts,
trace,
optimize_options,
diagnostics,
preserve_element_storage_layout,
testbench_random_seed,
component_libraries,
component_file_base,
)?;
Ok((program, dynamic_for_diagnostics))
}
#[cfg(feature = "systemverilog")]
pub fn parse_sv(
sources: &[(&str, &std::path::Path)],
top: &str,
parameter_overrides: &[(String, u64)],
_config: &BuildConfig,
ignored_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
)],
true_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
usize,
)],
four_state: bool,
trace_opts: &crate::debug::TraceOptions,
mut trace: Option<&mut crate::debug::CompilationTrace>,
optimize_options: &crate::optimizer::OptimizeOptions,
diagnostics: &crate::RuntimeDiagnostics,
preserve_element_storage_layout: bool,
testbench_random_seed: Option<u64>,
component_libraries: Vec<celox_testbench::ComponentLibrary>,
component_file_base: Option<std::path::PathBuf>,
) -> Result<crate::ir::OptimizedSir, ParserError> {
let frontend_trace_options = trace_opts.frontend(diagnostics);
let mut frontend_trace = celox_frontend_core::FrontendTrace::default();
let scheduled = celox_frontend_sv::schedule_sources(
sources,
top,
parameter_overrides,
ignored_loops,
true_loops,
four_state,
&frontend_trace_options,
trace.is_some().then_some(&mut frontend_trace),
)
.map_err(|error| match error {
celox_frontend_sv::FrontendError::Lowering(error) => error.into(),
celox_frontend_sv::FrontendError::Analyzer(error) => ParserError::unsupported(
64,
celox_frontend_veryl::LoweringPhase::SimulatorParser,
"systemverilog analysis",
error.to_string(),
None,
),
})?;
if let Some(trace) = trace.as_deref_mut() {
trace.absorb_frontend(frontend_trace);
}
finalize_scheduled_rtl(
scheduled,
None,
four_state,
trace_opts,
trace,
optimize_options,
diagnostics,
preserve_element_storage_layout,
testbench_random_seed,
component_libraries,
component_file_base,
)
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "systemverilog")]
pub fn parse_mixed(
top: &StrId,
ir: &veryl_analyzer::ir::Ir,
loop_provenance: &loop_provenance::LoopProvenance,
sv_sources: &[(&str, &std::path::Path)],
config: &BuildConfig,
ignored_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
)],
true_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
usize,
)],
four_state: bool,
trace_opts: &crate::debug::TraceOptions,
trace: Option<&mut crate::debug::CompilationTrace>,
optimize_options: &crate::optimizer::OptimizeOptions,
diagnostics: &crate::RuntimeDiagnostics,
preserve_element_storage_layout: bool,
testbench_random_seed: Option<u64>,
component_libraries: Vec<celox_testbench::ComponentLibrary>,
component_file_base: Option<std::path::PathBuf>,
) -> Result<
(
crate::ir::OptimizedSir,
Vec<celox_frontend_veryl::FrontendDiagnostic>,
),
ParserError,
> {
let external_roots = reachable_external_sv_roots(ir, top);
let external_roots = external_roots
.into_iter()
.map(|name| resource_table::get_str_value(name).unwrap_or_default())
.collect();
let external =
celox_frontend_sv::prepare_external_hierarchy(sv_sources, &external_roots, four_state)
.map_err(|error| match error {
celox_frontend_sv::FrontendError::Lowering(error) => error.into(),
celox_frontend_sv::FrontendError::Analyzer(error) => ParserError::unsupported(
64,
celox_frontend_veryl::LoweringPhase::SimulatorParser,
"systemverilog analysis",
error.to_string(),
None,
),
})?;
parse_with_external_hierarchy(
top,
ir,
loop_provenance,
&external,
config,
ignored_loops,
true_loops,
four_state,
trace_opts,
trace,
optimize_options,
diagnostics,
preserve_element_storage_layout,
testbench_random_seed,
component_libraries,
component_file_base,
)
}
#[allow(clippy::too_many_arguments)]
pub fn parse_with_external_hierarchy(
top: &StrId,
ir: &veryl_analyzer::ir::Ir,
loop_provenance: &loop_provenance::LoopProvenance,
external: &celox_frontend_core::symbolic::artifact::ExternalHierarchy,
config: &BuildConfig,
ignored_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
)],
true_loops: &[(
(Vec<(String, usize)>, Vec<String>),
(Vec<(String, usize)>, Vec<String>),
usize,
)],
four_state: bool,
trace_opts: &crate::debug::TraceOptions,
mut trace: Option<&mut crate::debug::CompilationTrace>,
optimize_options: &crate::optimizer::OptimizeOptions,
diagnostics: &crate::RuntimeDiagnostics,
preserve_element_storage_layout: bool,
testbench_random_seed: Option<u64>,
component_libraries: Vec<celox_testbench::ComponentLibrary>,
component_file_base: Option<std::path::PathBuf>,
) -> Result<
(
crate::ir::OptimizedSir,
Vec<celox_frontend_veryl::FrontendDiagnostic>,
),
ParserError,
> {
let symbolic = celox_frontend_veryl::parse_ir_with_external_hierarchy(
ir,
loop_provenance,
external,
config,
top,
)?;
if let Some(trace) = trace.as_deref_mut()
&& trace_opts.analyzer_ir
{
trace.analyzer_ir = Some(ir.to_string());
}
let frontend_trace_options = trace_opts.frontend(diagnostics);
let mut frontend_trace = celox_frontend_core::FrontendTrace::default();
let scheduled = celox_frontend_veryl::schedule_symbolic_rtl(
symbolic,
config,
ignored_loops,
true_loops,
four_state,
&frontend_trace_options,
trace.is_some().then_some(&mut frontend_trace),
)?;
if let Some(trace) = trace.as_deref_mut() {
trace.absorb_frontend(frontend_trace);
}
let dynamic_for_diagnostics = dynamic_for_diagnostics(&scheduled, ir);
let celox_frontend_veryl::VerylScheduledRtlOutput {
scheduled,
fused_optimization_hints,
testbench_source,
} = scheduled;
let program = finalize_scheduled_rtl(
celox_frontend_core::ScheduledRtlOutput {
scheduled,
fused_optimization_hints,
},
Some(testbench_source),
four_state,
trace_opts,
trace,
optimize_options,
diagnostics,
preserve_element_storage_layout,
testbench_random_seed,
component_libraries,
component_file_base,
)?;
Ok((program, dynamic_for_diagnostics))
}
#[cfg(feature = "systemverilog")]
fn reachable_external_sv_roots(ir: &veryl_analyzer::ir::Ir, top: &StrId) -> HashSet<StrId> {
use veryl_analyzer::ir::{Component, Declaration};
let Some(root) = ir
.components
.iter()
.rev()
.find_map(|component| match component {
Component::Module(module) if module.name == *top => Some(module),
_ => None,
})
else {
return HashSet::default();
};
let mut roots = HashSet::default();
let mut visited = HashSet::default();
let mut queue = vec![root];
while let Some(module) = queue.pop() {
if !visited.insert(std::ptr::from_ref(module) as usize) {
continue;
}
for declaration in &module.declarations {
let Declaration::Inst(instance) = declaration else {
continue;
};
match instance.component.as_ref() {
Component::SystemVerilog(system_verilog) => {
roots.insert(system_verilog.name);
}
Component::Module(child) => queue.push(child),
Component::Interface(_) => {}
}
}
}
roots
}