diff --git a/xlsynth-vastly/Cargo.toml b/xlsynth-vastly/Cargo.toml index 557adf05..50268b44 100644 --- a/xlsynth-vastly/Cargo.toml +++ b/xlsynth-vastly/Cargo.toml @@ -18,8 +18,9 @@ path = "src/lib.rs" [dependencies] clap = { version = "4.5.21", features = ["derive"] } -xlsynth = { path = "../xlsynth", version = "0.37.0" } +xlsynth = { path = "../xlsynth", version = "0.37.0", optional = true } [features] default = [] +irvals = ["dep:xlsynth"] reference-sim-tests = [] diff --git a/xlsynth-vastly/src/bin/vastly-sim-pipeline.rs b/xlsynth-vastly/src/bin/vastly-sim-pipeline.rs index 17ad1908..45fab26f 100644 --- a/xlsynth-vastly/src/bin/vastly-sim-pipeline.rs +++ b/xlsynth-vastly/src/bin/vastly-sim-pipeline.rs @@ -170,7 +170,11 @@ fn main_inner() -> xlsynth_vastly::Result<()> { let mut cov = CoverageCounters::default(); cov.defines = defines.clone(); for a in &m.combo.assigns { - cov.register_ternaries_from_spanned_expr(&a.rhs_spanned); + cov.register_ternaries_from_spanned_expr( + a.rhs_spanned + .as_ref() + .expect("coverage registration requires spanned assign expressions"), + ); } cov.register_functions(&m.fn_meta); let cover = compute_coverability_or_fallback_with_defines(&src_text, &defines); diff --git a/xlsynth-vastly/src/combo_compile.rs b/xlsynth-vastly/src/combo_compile.rs index 5237da88..08713c20 100644 --- a/xlsynth-vastly/src/combo_compile.rs +++ b/xlsynth-vastly/src/combo_compile.rs @@ -17,12 +17,9 @@ use crate::sv_ast::ParsedModule; pub fn compile_combo_module(src: &str) -> Result { let parse_src = src; - let parsed: ParsedModule = crate::sv_parser::parse_combo_module(parse_src)?; - let items = crate::generate_constructs::elaborate_combo_items( - parse_src, - &parsed.params, - &parsed.items, - )?; + let parsed: ParsedModule = crate::sv_parser::parse_combo_module(src)?; + let items = + crate::generate_constructs::elaborate_combo_items(src, &parsed.params, &parsed.items)?; let module_name = parsed.name; let (input_ports, output_ports, mut decls) = crate::compiled_module::lower_ports(&parsed.ports); @@ -37,20 +34,25 @@ pub fn compile_combo_module(src: &str) -> Result { match it { ModuleItem::Decl { .. } => {} ModuleItem::Assign { - lhs, rhs, rhs_text, .. + lhs, + rhs, + rhs_spanned, + rhs_span, + .. } => { assigns.push(crate::compiled_module::lower_assign( - parse_src, lhs, - *rhs, - rhs_text.as_deref(), + rhs, + rhs_spanned, + *rhs_span, &decls, + true, )?); } ModuleItem::Function { func: f, .. } => { functions.insert( f.name.clone(), - crate::compiled_module::lower_function(parse_src, f, &decls)?, + crate::compiled_module::lower_function(parse_src, f, &decls, true)?, ); } ModuleItem::AlwaysFf { .. } => { diff --git a/xlsynth-vastly/src/combo_eval.rs b/xlsynth-vastly/src/combo_eval.rs index 934eba60..f5c218c4 100644 --- a/xlsynth-vastly/src/combo_eval.rs +++ b/xlsynth-vastly/src/combo_eval.rs @@ -255,8 +255,12 @@ pub fn eval_combo_seeded_with_coverage( .get(lhs_base) .ok_or_else(|| Error::Parse(format!("no decl for assign lhs `{lhs_base}`")))?; let expected_width = lhs_expected_write_width(&a.lhs, info)?; + let rhs_spanned = a + .rhs_spanned + .as_ref() + .expect("coverage evaluation requires spanned assign expressions"); let rhs_v = eval_spanned_expr_with_funcs( - &a.rhs_spanned, + rhs_spanned, &env, &m.functions, expected_width, diff --git a/xlsynth-vastly/src/compiled_module.rs b/xlsynth-vastly/src/compiled_module.rs index 4152e349..1071cb88 100644 --- a/xlsynth-vastly/src/compiled_module.rs +++ b/xlsynth-vastly/src/compiled_module.rs @@ -13,7 +13,6 @@ use crate::ast_spanned::SpannedExpr; use crate::packed::rewrite_packed_expr; use crate::packed::rewrite_packed_lhs; use crate::packed::rewrite_packed_spanned_expr; -use crate::parser::parse_expr; use crate::parser_spanned::parse_expr_spanned; use crate::sv_ast::Decl; use crate::sv_ast::FunctionBody; @@ -53,7 +52,7 @@ pub struct ModuleAssign { pub lhs: Lhs, pub rhs: Expr, pub rhs_span: Span, - pub rhs_spanned: SpannedExpr, + pub rhs_spanned: Option, } impl ModuleAssign { @@ -210,18 +209,20 @@ pub(crate) fn extend_decls_from_items( /// Lowers an assign item using packed rewrites and span-preserving parsed AST. pub(crate) fn lower_assign( - parse_src: &str, lhs: &Lhs, + rhs: &Expr, + rhs_spanned: &SpannedExpr, rhs_span: Span, - rhs_text: Option<&str>, decls: &BTreeMap, + preserve_spans: bool, ) -> Result { - let rhs_src = rhs_text.unwrap_or_else(|| parse_src[rhs_span.start..rhs_span.end].trim()); let lhs = rewrite_packed_lhs(lhs.clone(), decls)?; - let rhs = rewrite_packed_expr(parse_expr(rhs_src)?, decls)?; - let mut rhs_spanned = parse_expr_spanned(rhs_src)?; - rhs_spanned.shift_spans(rhs_span.start); - rhs_spanned = rewrite_packed_spanned_expr(rhs_spanned, decls)?; + let rhs = rewrite_packed_expr(rhs.clone(), decls)?; + let rhs_spanned = if preserve_spans { + Some(rewrite_packed_spanned_expr(rhs_spanned.clone(), decls)?) + } else { + None + }; Ok(ModuleAssign { lhs, rhs, @@ -235,6 +236,7 @@ pub(crate) fn lower_function( parse_src: &str, f: &FunctionDecl, decls: &BTreeMap, + preserve_spans: bool, ) -> Result { let mut fn_decls = decls.clone(); for arg in &f.args { @@ -253,13 +255,10 @@ pub(crate) fn lower_function( let body = match &f.body { FunctionBody::UniqueCasez { selector, arms, .. } => { - let selector_src = parse_src[selector.start..selector.end].trim(); - let selector = rewrite_packed_expr(parse_expr(selector_src)?, &fn_decls)?; - + let selector = rewrite_packed_expr(selector.clone(), &fn_decls)?; let mut out_arms: Vec = Vec::new(); for a in arms { - let value_src = parse_src[a.value.start..a.value.end].trim(); - let value = rewrite_packed_expr(parse_expr(value_src)?, &fn_decls)?; + let value = rewrite_packed_expr(a.value.clone(), &fn_decls)?; let pat = a.pat.as_ref().map(|p| CasezPattern { width: p.width, bits_msb: p.bits_msb.clone(), @@ -271,22 +270,22 @@ pub(crate) fn lower_function( arms: out_arms, } } - FunctionBody::Assign { value } => { - let value_src = parse_src[value.start..value.end].trim(); - let expr = rewrite_packed_expr(parse_expr(value_src)?, &fn_decls)?; - let mut expr_spanned = parse_expr_spanned(value_src)?; - expr_spanned.shift_spans(value.start); - expr_spanned = rewrite_packed_spanned_expr(expr_spanned, &fn_decls)?; - CompiledFunctionBody::Expr { - expr, - expr_spanned: Some(expr_spanned), - } + FunctionBody::Assign { value, value_span } => { + let expr = rewrite_packed_expr(value.clone(), &fn_decls)?; + let expr_spanned = if preserve_spans { + Some(rewrite_packed_spanned_expr( + parse_expr_spanned_in_span(parse_src, *value_span)?, + &fn_decls, + )?) + } else { + None + }; + CompiledFunctionBody::Expr { expr, expr_spanned } } FunctionBody::Procedure { assigns } => { let mut out_assigns: Vec = Vec::with_capacity(assigns.len()); for a in assigns { - let value_src = parse_src[a.value.start..a.value.end].trim(); - let expr = rewrite_packed_expr(parse_expr(value_src)?, &fn_decls)?; + let expr = rewrite_packed_expr(a.value.clone(), &fn_decls)?; out_assigns.push(FunctionAssign { lhs: a.lhs.clone(), expr, @@ -312,6 +311,13 @@ pub(crate) fn lower_function( }) } +fn parse_expr_spanned_in_span(parse_src: &str, span: Span) -> Result { + let expr_src = parse_src[span.start..span.end].trim(); + let mut spanned = parse_expr_spanned(expr_src)?; + spanned.shift_spans(span.start); + Ok(spanned) +} + fn decl_info_from_decl(d: &Decl) -> DeclInfo { DeclInfo { width: d.width, diff --git a/xlsynth-vastly/src/eval.rs b/xlsynth-vastly/src/eval.rs index 26816b43..6849e1a5 100644 --- a/xlsynth-vastly/src/eval.rs +++ b/xlsynth-vastly/src/eval.rs @@ -41,15 +41,9 @@ pub(crate) fn operand_with_own_sign_ctx( expected_width: Option, expected_signedness: Option, ) -> Value4 { - let v = if let Some(signedness) = expected_signedness { - v.with_signedness(signedness) - } else { - v - }; - let Some(width) = expected_width else { - return v; - }; - if width <= v.width { v } else { v.resize(width) } + let signedness = expected_signedness.unwrap_or(v.signedness); + let width = expected_width.unwrap_or(v.width).max(v.width); + v.into_width_and_signedness(width, signedness) } pub(crate) fn replication_count_to_u32(v: &Value4) -> Result { @@ -75,8 +69,8 @@ fn operand_with_merged_sign_ctx( ) -> (Value4, Value4) { let width = expected_width.unwrap_or(0).max(lhs.width.max(rhs.width)); let signedness = expected_signedness.unwrap_or_else(|| merged_signedness(&lhs, &rhs)); - let lhs = lhs.with_signedness(signedness).resize(width); - let rhs = rhs.with_signedness(signedness).resize(width); + let lhs = lhs.into_width_and_signedness(width, signedness); + let rhs = rhs.into_width_and_signedness(width, signedness); (lhs, rhs) } diff --git a/xlsynth-vastly/src/fixture_runner.rs b/xlsynth-vastly/src/fixture_runner.rs new file mode 100644 index 00000000..9a0d5056 --- /dev/null +++ b/xlsynth-vastly/src/fixture_runner.rs @@ -0,0 +1,756 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; +use std::collections::BTreeSet; + +use crate::ComboEvalPlan; +use crate::CompiledPipelineModule; +use crate::Env; +use crate::LogicBit; +use crate::Result; +use crate::Signedness; +use crate::State; +use crate::Value4; +use crate::ast::Expr; +use crate::compiled_module::CompiledFunction; +use crate::compiled_module::CompiledFunctionBody; +use crate::eval_combo_seeded; +use crate::packed::packed_index_selection_if_in_bounds; +use crate::plan_combo_eval; +use crate::step_pipeline_state_with_env; + +#[derive(Debug, Clone)] +pub struct InputPortHandle { + name: String, + width: u32, + signedness: Signedness, + decl: crate::compiled_module::DeclInfo, +} + +impl InputPortHandle { + pub fn name(&self) -> &str { + &self.name + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn signedness(&self) -> Signedness { + self.signedness + } +} + +#[derive(Debug, Clone)] +pub struct OutputPortHandle { + name: String, + width: u32, + signedness: Signedness, + decl: crate::compiled_module::DeclInfo, +} + +impl OutputPortHandle { + pub fn name(&self) -> &str { + &self.name + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn signedness(&self) -> Signedness { + self.signedness + } +} + +pub struct PortBindings<'a> { + module: &'a CompiledPipelineModule, +} + +impl<'a> PortBindings<'a> { + pub fn new(module: &'a CompiledPipelineModule) -> Self { + Self { module } + } + + pub fn maybe_input(&self, name: &str) -> Option { + self.module + .combo + .input_ports + .iter() + .find(|port| port.name == name) + .and_then(|port| { + self.module + .combo + .decls + .get(name) + .map(|decl| InputPortHandle { + name: name.to_string(), + width: port.width, + signedness: decl.signedness, + decl: decl.clone(), + }) + }) + } + + pub fn input(&self, name: &str) -> Result { + if name == self.module.clk_name { + return Err(crate::Error::Parse(format!( + "clock `{name}` is managed internally and is not exposed as a fixture input" + ))); + } + self.maybe_input(name) + .ok_or_else(|| crate::Error::Parse(format!("input port `{name}` was not found"))) + } + + pub fn input_with_width(&self, name: &str, width: u32) -> Result { + let handle = self.input(name)?; + if handle.width != width { + return Err(crate::Error::Parse(format!( + "input port `{name}` has width {}, expected {width}", + handle.width + ))); + } + Ok(handle) + } + + pub fn output(&self, name: &str) -> Result { + self.maybe_output(name) + .ok_or_else(|| crate::Error::Parse(format!("output port `{name}` was not found"))) + } + + pub fn output_with_width(&self, name: &str, width: u32) -> Result { + let handle = self.output(name)?; + if handle.width != width { + return Err(crate::Error::Parse(format!( + "output port `{name}` has width {}, expected {width}", + handle.width + ))); + } + Ok(handle) + } + + pub fn maybe_output(&self, name: &str) -> Option { + self.module + .combo + .output_ports + .iter() + .find(|port| port.name == name) + .and_then(|port| { + self.module + .combo + .decls + .get(name) + .map(|decl| OutputPortHandle { + name: name.to_string(), + width: port.width, + signedness: decl.signedness, + decl: decl.clone(), + }) + }) + } +} + +pub trait CycleFixture { + fn bind(&mut self, ports: &PortBindings<'_>) -> Result<()>; + + fn drive_cycle_inputs(&mut self, _cycle: u64, _ctx: &mut DriveContext<'_>) -> Result<()> { + Ok(()) + } + + fn observe_cycle_outputs(&mut self, _cycle: u64, _ctx: &ObserveContext<'_>) -> Result<()> { + Ok(()) + } +} + +pub struct DriveContext<'a> { + inputs: &'a mut BTreeMap, + driven_bits: &'a mut BTreeMap>, + previous_outputs: &'a BTreeMap, +} + +impl<'a> DriveContext<'a> { + pub fn previous_output(&self, handle: &OutputPortHandle) -> Option<&Value4> { + self.previous_outputs.get(handle.name()) + } + + pub fn set(&mut self, handle: &InputPortHandle, value: Value4) -> Result<()> { + if value.width != handle.width { + return Err(crate::Error::Parse(format!( + "input port `{}` expects width {}, got {}", + handle.name, handle.width, value.width + ))); + } + self.claim_range( + handle.name(), + handle.width, + 0, + handle.width, + format!("input port `{}`", handle.name()), + )?; + self.inputs.insert( + handle.name.clone(), + value.with_signedness(handle.signedness), + ); + Ok(()) + } + + pub fn set_zero(&mut self, handle: &InputPortHandle) -> Result<()> { + self.set(handle, Value4::zeros(handle.width, handle.signedness)) + } + + pub fn set_bool(&mut self, handle: &InputPortHandle, value: bool) -> Result<()> { + self.set( + handle, + Value4::from_u64(handle.width, handle.signedness, u64::from(value)), + ) + } + + pub fn set_u64(&mut self, handle: &InputPortHandle, value: u64) -> Result<()> { + self.set( + handle, + Value4::from_u64(handle.width, handle.signedness, value), + ) + } + + pub fn set_packed( + &mut self, + handle: &InputPortHandle, + indices: &[u32], + value: Value4, + ) -> Result<()> { + let (offset, width) = selected_range(&handle.decl, handle.name(), indices)?; + if value.width != width { + return Err(crate::Error::Parse(format!( + "input port `{}` slice {:?} expects width {}, got {}", + handle.name, indices, width, value.width + ))); + } + self.claim_range( + handle.name(), + handle.width, + offset, + width, + format!("input port `{}` slice {:?}", handle.name(), indices), + )?; + let current = self + .inputs + .get(handle.name()) + .cloned() + .unwrap_or_else(|| Value4::zeros(handle.width, handle.signedness)); + self.inputs.insert( + handle.name.clone(), + current.replace_slice(offset, &value.with_signedness(handle.signedness))?, + ); + Ok(()) + } + + pub fn set_packed_u64( + &mut self, + handle: &InputPortHandle, + indices: &[u32], + value: u64, + ) -> Result<()> { + let (_, width) = selected_range(&handle.decl, handle.name(), indices)?; + self.set_packed( + handle, + indices, + Value4::from_u64(width, handle.signedness, value), + ) + } + + pub fn set_packed_bool( + &mut self, + handle: &InputPortHandle, + indices: &[u32], + value: bool, + ) -> Result<()> { + self.set_packed_u64(handle, indices, u64::from(value)) + } + + fn claim_range( + &mut self, + name: &str, + port_width: u32, + offset: u32, + width: u32, + target_desc: String, + ) -> Result<()> { + let bits = self + .driven_bits + .entry(name.to_string()) + .or_insert_with(|| vec![false; port_width as usize]); + let start = offset as usize; + let end = (offset + width) as usize; + if bits[start..end].iter().any(|bit| *bit) { + return Err(crate::Error::Parse(format!( + "{target_desc} was already driven earlier in the cycle" + ))); + } + for bit in &mut bits[start..end] { + *bit = true; + } + Ok(()) + } +} + +pub struct ObserveContext<'a> { + inputs: &'a BTreeMap, + outputs: &'a BTreeMap, +} + +impl<'a> ObserveContext<'a> { + pub fn input(&self, handle: &InputPortHandle) -> Result<&Value4> { + self.inputs.get(handle.name()).ok_or_else(|| { + crate::Error::Parse(format!( + "input port `{}` has no driven value", + handle.name() + )) + }) + } + + pub fn output(&self, handle: &OutputPortHandle) -> Result<&Value4> { + self.outputs.get(handle.name()).ok_or_else(|| { + crate::Error::Parse(format!( + "output port `{}` has no sampled value", + handle.name() + )) + }) + } + + pub fn output_u64_if_known(&self, handle: &OutputPortHandle) -> Result> { + Ok(self.output(handle)?.to_u64_if_known()) + } + + pub fn output_bool_if_known(&self, handle: &OutputPortHandle) -> Result> { + let value = self.output(handle)?; + if value.width != 1 { + return Err(crate::Error::Parse(format!( + "output port `{}` is width {}, not width 1", + handle.name(), + value.width + ))); + } + Ok(match value.bit(0) { + LogicBit::Zero => Some(false), + LogicBit::One => Some(true), + LogicBit::X | LogicBit::Z => None, + }) + } + + pub fn output_packed(&self, handle: &OutputPortHandle, indices: &[u32]) -> Result { + let (offset, width) = selected_range(&handle.decl, handle.name(), indices)?; + self.output(handle)?.slice_lsb_width(offset, width) + } + + pub fn output_packed_u64_if_known( + &self, + handle: &OutputPortHandle, + indices: &[u32], + ) -> Result> { + Ok(self.output_packed(handle, indices)?.to_u64_if_known()) + } + + pub fn output_packed_bool_if_known( + &self, + handle: &OutputPortHandle, + indices: &[u32], + ) -> Result> { + let value = self.output_packed(handle, indices)?; + if value.width != 1 { + return Err(crate::Error::Parse(format!( + "output port `{}` slice {:?} is width {}, not width 1", + handle.name(), + indices, + value.width + ))); + } + Ok(match value.bit(0) { + LogicBit::Zero => Some(false), + LogicBit::One => Some(true), + LogicBit::X | LogicBit::Z => None, + }) + } +} + +#[derive(Debug, Clone)] +pub struct FixtureCycleResult { + pub cycle: u64, + pub inputs: BTreeMap, + pub pre_outputs: BTreeMap, + pub outputs: BTreeMap, +} + +pub struct FixtureRunner { + module: CompiledPipelineModule, + plan: ComboEvalPlan, + state: State, + cycle: u64, + last_inputs: BTreeMap, + last_outputs: BTreeMap, +} + +impl FixtureRunner { + pub fn new(module: &CompiledPipelineModule) -> Result { + let module = module.clone(); + reject_clock_sensitive_outputs(&module)?; + let plan = plan_combo_eval(&module.combo)?; + Ok(Self { + last_inputs: zero_inputs(&module)?, + plan, + state: module.initial_state_x(), + cycle: 0, + module, + last_outputs: BTreeMap::new(), + }) + } + + pub fn port_bindings(&self) -> PortBindings<'_> { + PortBindings::new(&self.module) + } + + pub fn bind_fixture(&self, fixture: &mut dyn CycleFixture) -> Result<()> { + fixture.bind(&self.port_bindings()) + } + + pub fn bind_fixtures(&self, fixtures: &mut [&mut dyn CycleFixture]) -> Result<()> { + let ports = self.port_bindings(); + for fixture in fixtures { + fixture.bind(&ports)?; + } + Ok(()) + } + + pub fn cycle(&self) -> u64 { + self.cycle + } + + pub fn last_inputs(&self) -> &BTreeMap { + &self.last_inputs + } + + pub fn last_outputs(&self) -> &BTreeMap { + &self.last_outputs + } + + pub fn last_observe_context(&self) -> ObserveContext<'_> { + ObserveContext { + inputs: &self.last_inputs, + outputs: &self.last_outputs, + } + } + + pub fn reset_state_x(&mut self) { + self.state = self.module.initial_state_x(); + self.last_inputs.clear(); + self.last_outputs.clear(); + self.cycle = 0; + } + + pub fn step_cycle( + &mut self, + fixtures: &mut [&mut dyn CycleFixture], + ) -> Result { + self.step_cycle_with_drive(fixtures, |_ctx| Ok(())) + } + + pub fn step_cycle_with_drive( + &mut self, + fixtures: &mut [&mut dyn CycleFixture], + extra_drive: F, + ) -> Result + where + F: FnOnce(&mut DriveContext<'_>) -> Result<()>, + { + let mut inputs = zero_inputs(&self.module)?; + let mut driven_bits = BTreeMap::new(); + { + let mut drive_ctx = DriveContext { + inputs: &mut inputs, + driven_bits: &mut driven_bits, + previous_outputs: &self.last_outputs, + }; + for fixture in fixtures.iter_mut() { + fixture.drive_cycle_inputs(self.cycle, &mut drive_ctx)?; + } + extra_drive(&mut drive_ctx)?; + } + + let (pre_outputs, outputs) = self.simulate_cycle(&inputs)?; + let result = FixtureCycleResult { + cycle: self.cycle, + inputs: inputs.clone(), + pre_outputs: pre_outputs.clone(), + outputs: outputs.clone(), + }; + { + let observe_ctx = ObserveContext { + inputs: &inputs, + outputs: &outputs, + }; + for fixture in fixtures.iter_mut() { + fixture.observe_cycle_outputs(self.cycle, &observe_ctx)?; + } + } + + self.last_inputs = inputs; + self.last_outputs = outputs; + self.cycle += 1; + Ok(result) + } + + fn simulate_cycle( + &mut self, + inputs: &BTreeMap, + ) -> Result<(BTreeMap, BTreeMap)> { + let high_seed = build_seed_env(&self.module, &self.state, inputs, true); + let high_values = eval_combo_seeded(&self.module.combo, &self.plan, &high_seed)?; + if self.module.seqs.is_empty() { + return Ok((high_values.clone(), high_values)); + } + let env = env_from_values(&high_values); + self.state = step_pipeline_state_with_env(&self.module, &env, &self.state)?; + let post_seed = build_seed_env(&self.module, &self.state, inputs, true); + let post_values = eval_combo_seeded(&self.module.combo, &self.plan, &post_seed)?; + Ok((high_values, post_values)) + } +} + +fn zero_inputs(module: &CompiledPipelineModule) -> Result> { + let mut out = BTreeMap::new(); + for port in &module.combo.input_ports { + let decl = module.combo.decls.get(&port.name).ok_or_else(|| { + crate::Error::Parse(format!("no decl info found for input `{}`", port.name)) + })?; + out.insert( + port.name.clone(), + Value4::zeros(decl.width, decl.signedness), + ); + } + Ok(out) +} + +fn build_seed_env( + module: &CompiledPipelineModule, + state: &State, + inputs: &BTreeMap, + clk_high: bool, +) -> Env { + let mut seed = Env::new(); + for (name, value) in state { + seed.insert(name.clone(), value.clone()); + } + for (name, value) in inputs { + seed.insert(name.clone(), value.clone()); + } + seed.insert( + module.clk_name.clone(), + Value4::from_u64(1, Signedness::Unsigned, u64::from(clk_high)), + ); + seed +} + +fn env_from_values(values: &BTreeMap) -> Env { + let mut env = Env::new(); + for (name, value) in values { + env.insert(name.clone(), value.clone()); + } + env +} + +fn reject_clock_sensitive_outputs(module: &CompiledPipelineModule) -> Result<()> { + let mut analyzer = ClockDependencyAnalyzer::new(module); + let sensitive_outputs = analyzer.clock_sensitive_outputs()?; + if sensitive_outputs.is_empty() { + return Ok(()); + } + Err(crate::Error::Parse(format!( + "fixture runner does not support combinational outputs that depend on clock `{}`: {}", + module.clk_name, + sensitive_outputs.join(", ") + ))) +} + +struct ClockDependencyAnalyzer<'a> { + module: &'a CompiledPipelineModule, + ident_memo: BTreeMap, + ident_visiting: BTreeSet, + function_memo: BTreeMap, + function_visiting: BTreeSet, +} + +impl<'a> ClockDependencyAnalyzer<'a> { + fn new(module: &'a CompiledPipelineModule) -> Self { + Self { + module, + ident_memo: BTreeMap::new(), + ident_visiting: BTreeSet::new(), + function_memo: BTreeMap::new(), + function_visiting: BTreeSet::new(), + } + } + + /// Returns output ports whose combinational value can vary with the + /// internal clock phase. + fn clock_sensitive_outputs(&mut self) -> Result> { + let mut out = Vec::new(); + for port in &self.module.combo.output_ports { + if self.ident_depends_on_clock(&port.name)? { + out.push(port.name.clone()); + } + } + Ok(out) + } + + fn ident_depends_on_clock(&mut self, name: &str) -> Result { + if name == self.module.clk_name { + return Ok(true); + } + if let Some(depends) = self.ident_memo.get(name) { + return Ok(*depends); + } + if !self.ident_visiting.insert(name.to_string()) { + // Combo assign cycles are rejected earlier; treat any unexpected re-entry + // conservatively so the fixture runner stays sound. + return Ok(true); + } + + let mut depends = false; + for assign in &self.module.combo.assigns { + if assign.lhs_base() != name { + continue; + } + if self.expr_depends_on_clock(&assign.rhs, &BTreeMap::new())? { + depends = true; + break; + } + } + + self.ident_visiting.remove(name); + self.ident_memo.insert(name.to_string(), depends); + Ok(depends) + } + + fn function_depends_on_clock(&mut self, name: &str) -> Result { + if let Some(depends) = self.function_memo.get(name) { + return Ok(*depends); + } + if !self.function_visiting.insert(name.to_string()) { + // Recursive helper analysis is not expected here; reject recursively + // referenced functions conservatively. + return Ok(true); + } + + let function = self + .module + .combo + .functions + .get(name) + .ok_or_else(|| crate::Error::Parse(format!("unknown function `{name}`")))?; + let depends = self.function_body_depends_on_clock(function)?; + + self.function_visiting.remove(name); + self.function_memo.insert(name.to_string(), depends); + Ok(depends) + } + + fn function_body_depends_on_clock(&mut self, function: &CompiledFunction) -> Result { + let mut local_deps = BTreeMap::new(); + for arg in &function.args { + local_deps.insert(arg.name.clone(), false); + } + for name in function.locals.keys() { + local_deps.insert(name.clone(), false); + } + local_deps.insert(function.name.clone(), false); + + match &function.body { + CompiledFunctionBody::Casez { selector, arms } => { + if self.expr_depends_on_clock(selector, &local_deps)? { + return Ok(true); + } + for arm in arms { + if self.expr_depends_on_clock(&arm.value, &local_deps)? { + return Ok(true); + } + } + Ok(false) + } + CompiledFunctionBody::Expr { expr, .. } => { + self.expr_depends_on_clock(expr, &local_deps) + } + CompiledFunctionBody::Procedure { assigns } => { + for assign in assigns { + let depends = self.expr_depends_on_clock(&assign.expr, &local_deps)?; + local_deps.insert(assign.lhs.clone(), depends); + } + Ok(local_deps.get(&function.name).copied().unwrap_or(false)) + } + } + } + + fn expr_depends_on_clock( + &mut self, + expr: &Expr, + local_deps: &BTreeMap, + ) -> Result { + match expr { + Expr::Ident(name) => Ok(local_deps + .get(name) + .copied() + .unwrap_or(self.ident_depends_on_clock(name)?)), + Expr::Literal(_) | Expr::UnsizedNumber(_) | Expr::UnbasedUnsized(_) => Ok(false), + Expr::Call { name, args } => { + for arg in args { + if self.expr_depends_on_clock(arg, local_deps)? { + return Ok(true); + } + } + if name == "$signed" || name == "$unsigned" { + return Ok(false); + } + self.function_depends_on_clock(name) + } + Expr::Concat(parts) => { + for part in parts { + if self.expr_depends_on_clock(part, local_deps)? { + return Ok(true); + } + } + Ok(false) + } + Expr::Replicate { count, expr } => Ok(self.expr_depends_on_clock(count, local_deps)? + || self.expr_depends_on_clock(expr, local_deps)?), + Expr::Cast { width, expr } => Ok(self.expr_depends_on_clock(width, local_deps)? + || self.expr_depends_on_clock(expr, local_deps)?), + Expr::Index { expr, index } => Ok(self.expr_depends_on_clock(expr, local_deps)? + || self.expr_depends_on_clock(index, local_deps)?), + Expr::Slice { expr, msb, lsb } => Ok(self.expr_depends_on_clock(expr, local_deps)? + || self.expr_depends_on_clock(msb, local_deps)? + || self.expr_depends_on_clock(lsb, local_deps)?), + Expr::IndexedSlice { + expr, base, width, .. + } => Ok(self.expr_depends_on_clock(expr, local_deps)? + || self.expr_depends_on_clock(base, local_deps)? + || self.expr_depends_on_clock(width, local_deps)?), + Expr::Unary { expr, .. } => self.expr_depends_on_clock(expr, local_deps), + Expr::Binary { lhs, rhs, .. } => Ok(self.expr_depends_on_clock(lhs, local_deps)? + || self.expr_depends_on_clock(rhs, local_deps)?), + Expr::Ternary { cond, t, f } => Ok(self.expr_depends_on_clock(cond, local_deps)? + || self.expr_depends_on_clock(t, local_deps)? + || self.expr_depends_on_clock(f, local_deps)?), + } + } +} + +fn selected_range( + decl: &crate::compiled_module::DeclInfo, + name: &str, + indices: &[u32], +) -> Result<(u32, u32)> { + packed_index_selection_if_in_bounds(decl, indices)?.ok_or_else(|| { + crate::Error::Parse(format!( + "packed indices {:?} are out of bounds for `{name}`", + indices + )) + }) +} diff --git a/xlsynth-vastly/src/generate_constructs.rs b/xlsynth-vastly/src/generate_constructs.rs index 5ad23c28..0cd8e485 100644 --- a/xlsynth-vastly/src/generate_constructs.rs +++ b/xlsynth-vastly/src/generate_constructs.rs @@ -8,6 +8,8 @@ use crate::Result; use crate::Signedness; use crate::Value4; use crate::ast::Expr; +use crate::ast_spanned::SpannedExpr; +use crate::ast_spanned::SpannedExprKind; use crate::eval::eval_ast_with_calls; use crate::sv_ast::AlwaysFf; use crate::sv_ast::GenerateBranch; @@ -24,7 +26,10 @@ pub fn elaborate_combo_items( for (name, value) in params { env.insert(name.clone(), value.clone()); } - elaborate_items_impl(src, items, &env, &BTreeMap::new(), false) + let mut substs: BTreeMap = BTreeMap::new(); + let mut out = Vec::new(); + elaborate_items_impl(src, items, &mut env, &mut substs, false, &mut out)?; + Ok(out) } pub fn elaborate_pipeline_items( @@ -36,28 +41,31 @@ pub fn elaborate_pipeline_items( for (name, value) in params { env.insert(name.clone(), value.clone()); } - elaborate_items_impl(src, items, &env, &BTreeMap::new(), false) + let mut substs: BTreeMap = BTreeMap::new(); + let mut out = Vec::new(); + elaborate_items_impl(src, items, &mut env, &mut substs, false, &mut out)?; + Ok(out) } fn elaborate_items_impl( src: &str, items: &[ModuleItem], - env: &crate::Env, - substs: &BTreeMap, + env: &mut crate::Env, + substs: &mut BTreeMap, in_generate: bool, -) -> Result> { - let mut out = Vec::new(); + out: &mut Vec, +) -> Result<()> { for item in items { - elaborate_item(src, item, env, substs, in_generate, &mut out)?; + elaborate_item(src, item, env, substs, in_generate, out)?; } - Ok(out) + Ok(()) } fn elaborate_item( src: &str, item: &ModuleItem, - env: &crate::Env, - substs: &BTreeMap, + env: &mut crate::Env, + substs: &mut BTreeMap, in_generate: bool, out: &mut Vec, ) -> Result<()> { @@ -73,21 +81,15 @@ fn elaborate_item( ModuleItem::Assign { lhs, rhs, - rhs_text, + rhs_spanned, + rhs_span, span, } => { - let base_text = rhs_text - .as_deref() - .unwrap_or_else(|| src[rhs.start..rhs.end].trim()); - let substituted_text = if substs.is_empty() { - None - } else { - Some(substitute_text(base_text, substs)) - }; out.push(ModuleItem::Assign { lhs: substitute_lhs(lhs, substs), - rhs: *rhs, - rhs_text: substituted_text, + rhs: substitute_expr(rhs, substs), + rhs_spanned: substitute_spanned_expr(rhs_spanned, substs), + rhs_span: *rhs_span, span: *span, }); } @@ -120,34 +122,45 @@ fn elaborate_item( let limit_u = eval_const_u32(limit, env)?; for idx in start_u..limit_u { let genvar_value = u32_value(idx); - let mut next_env = env.clone(); - next_env.insert(genvar.clone(), genvar_value.clone()); - let mut next_substs = substs.clone(); - next_substs.insert(genvar.clone(), genvar_value); - out.extend(elaborate_items_impl( - src, - body, - &next_env, - &next_substs, - true, - )?); + let prev_env = env.insert(genvar.clone(), genvar_value.clone()); + let prev_subst = substs.insert(genvar.clone(), idx); + let elaborate_result = elaborate_items_impl(src, body, env, substs, true, out); + restore_env_binding(env, genvar, prev_env); + restore_subst_binding(substs, genvar, prev_subst); + elaborate_result?; } } ModuleItem::GenerateIf { branches, span: _ } => { if let Some(selected) = select_branch(branches, env)? { - out.extend(elaborate_items_impl( - src, - &selected.body, - env, - substs, - true, - )?); + elaborate_items_impl(src, &selected.body, env, substs, true, out)?; } } } Ok(()) } +fn restore_env_binding(env: &mut crate::Env, key: &str, previous: Option) { + match previous { + Some(value) => { + env.insert(key.to_string(), value); + } + None => { + env.remove(key); + } + } +} + +fn restore_subst_binding(map: &mut BTreeMap, key: &str, previous: Option) { + match previous { + Some(value) => { + map.insert(key.to_string(), value); + } + None => { + map.remove(key); + } + } +} + fn select_branch<'a>( branches: &'a [GenerateBranch], env: &crate::Env, @@ -187,12 +200,15 @@ fn u32_value(value: u32) -> Value4 { Value4::parse_numeric_token(32, Signedness::Unsigned, &value.to_string()).unwrap() } -fn substitute_expr(expr: &Expr, substs: &BTreeMap) -> Expr { +fn unsized_decimal_value(value: u32) -> Value4 { + Value4::parse_unsized_decimal_token(Signedness::Signed, &value.to_string()).unwrap() +} + +fn substitute_expr(expr: &Expr, substs: &BTreeMap) -> Expr { match expr { Expr::Ident(name) => substs .get(name) - .cloned() - .map(Expr::Literal) + .map(|value| Expr::UnsizedNumber(unsized_decimal_value(*value))) .unwrap_or_else(|| Expr::Ident(name.clone())), Expr::Literal(v) => Expr::Literal(v.clone()), Expr::UnsizedNumber(v) => Expr::UnsizedNumber(v.clone()), @@ -255,7 +271,78 @@ fn substitute_expr(expr: &Expr, substs: &BTreeMap) -> Expr { } } -fn substitute_lhs(lhs: &Lhs, substs: &BTreeMap) -> Lhs { +fn substitute_spanned_expr(expr: &SpannedExpr, substs: &BTreeMap) -> SpannedExpr { + let kind = match &expr.kind { + SpannedExprKind::Ident(name) => substs + .get(name) + .map(|value| SpannedExprKind::UnsizedNumber(unsized_decimal_value(*value))) + .unwrap_or_else(|| SpannedExprKind::Ident(name.clone())), + SpannedExprKind::Literal(v) => SpannedExprKind::Literal(v.clone()), + SpannedExprKind::UnsizedNumber(v) => SpannedExprKind::UnsizedNumber(v.clone()), + SpannedExprKind::UnbasedUnsized(bit) => SpannedExprKind::UnbasedUnsized(*bit), + SpannedExprKind::Call { name, args } => SpannedExprKind::Call { + name: name.clone(), + args: args + .iter() + .map(|arg| substitute_spanned_expr(arg, substs)) + .collect(), + }, + SpannedExprKind::Concat(parts) => SpannedExprKind::Concat( + parts + .iter() + .map(|part| substitute_spanned_expr(part, substs)) + .collect(), + ), + SpannedExprKind::Replicate { count, expr } => SpannedExprKind::Replicate { + count: Box::new(substitute_spanned_expr(count, substs)), + expr: Box::new(substitute_spanned_expr(expr, substs)), + }, + SpannedExprKind::Cast { width, expr } => SpannedExprKind::Cast { + width: Box::new(substitute_spanned_expr(width, substs)), + expr: Box::new(substitute_spanned_expr(expr, substs)), + }, + SpannedExprKind::Index { expr, index } => SpannedExprKind::Index { + expr: Box::new(substitute_spanned_expr(expr, substs)), + index: Box::new(substitute_spanned_expr(index, substs)), + }, + SpannedExprKind::Slice { expr, msb, lsb } => SpannedExprKind::Slice { + expr: Box::new(substitute_spanned_expr(expr, substs)), + msb: Box::new(substitute_spanned_expr(msb, substs)), + lsb: Box::new(substitute_spanned_expr(lsb, substs)), + }, + SpannedExprKind::IndexedSlice { + expr, + base, + width, + upward, + } => SpannedExprKind::IndexedSlice { + expr: Box::new(substitute_spanned_expr(expr, substs)), + base: Box::new(substitute_spanned_expr(base, substs)), + width: Box::new(substitute_spanned_expr(width, substs)), + upward: *upward, + }, + SpannedExprKind::Unary { op, expr } => SpannedExprKind::Unary { + op: *op, + expr: Box::new(substitute_spanned_expr(expr, substs)), + }, + SpannedExprKind::Binary { op, lhs, rhs } => SpannedExprKind::Binary { + op: *op, + lhs: Box::new(substitute_spanned_expr(lhs, substs)), + rhs: Box::new(substitute_spanned_expr(rhs, substs)), + }, + SpannedExprKind::Ternary { cond, t, f } => SpannedExprKind::Ternary { + cond: Box::new(substitute_spanned_expr(cond, substs)), + t: Box::new(substitute_spanned_expr(t, substs)), + f: Box::new(substitute_spanned_expr(f, substs)), + }, + }; + SpannedExpr { + span: expr.span, + kind, + } +} + +fn substitute_lhs(lhs: &Lhs, substs: &BTreeMap) -> Lhs { match lhs { Lhs::Ident(name) => Lhs::Ident(name.clone()), Lhs::Index { base, index } => Lhs::Index { @@ -277,7 +364,7 @@ fn substitute_lhs(lhs: &Lhs, substs: &BTreeMap) -> Lhs { } } -fn substitute_stmt(stmt: &Stmt, substs: &BTreeMap) -> Stmt { +fn substitute_stmt(stmt: &Stmt, substs: &BTreeMap) -> Stmt { match stmt { Stmt::Begin(stmts) => Stmt::Begin( stmts @@ -310,48 +397,3 @@ fn substitute_stmt(stmt: &Stmt, substs: &BTreeMap) -> Stmt { Stmt::Empty => Stmt::Empty, } } - -fn substitute_text(text: &str, substs: &BTreeMap) -> String { - let mut out = String::from(text); - for (name, value) in substs { - let replacement = value - .to_u32_if_known() - .map(|v| v.to_string()) - .unwrap_or_else(|| value.to_bit_string_msb_first()); - out = replace_ident_token(&out, name, &replacement); - } - out -} - -fn replace_ident_token(s: &str, target: &str, replacement: &str) -> String { - let bytes = s.as_bytes(); - let mut out = String::with_capacity(s.len()); - let mut i = 0usize; - while i < bytes.len() { - if is_ident_start(bytes[i]) { - let start = i; - i += 1; - while i < bytes.len() && is_ident_continue(bytes[i]) { - i += 1; - } - let ident = &s[start..i]; - if ident == target { - out.push_str(replacement); - } else { - out.push_str(ident); - } - } else { - out.push(bytes[i] as char); - i += 1; - } - } - out -} - -fn is_ident_start(b: u8) -> bool { - b == b'_' || b.is_ascii_alphabetic() -} - -fn is_ident_continue(b: u8) -> bool { - b == b'_' || b.is_ascii_alphanumeric() -} diff --git a/xlsynth-vastly/src/lib.rs b/xlsynth-vastly/src/lib.rs index 24ea3bf1..7d0551c0 100644 --- a/xlsynth-vastly/src/lib.rs +++ b/xlsynth-vastly/src/lib.rs @@ -17,7 +17,9 @@ mod coverability; mod coverage; mod coverage_render2; mod eval; +mod fixture_runner; mod generate_constructs; +#[cfg(feature = "irvals")] mod irvals; mod iverilog_combo; mod lexer; @@ -66,7 +68,26 @@ pub use crate::coverage::SpanKey; pub use crate::coverage_render2::render_annotated_source; pub use crate::eval::EvalObserver; pub use crate::eval::eval_expr; +pub use crate::fixture_runner::CycleFixture; +pub use crate::fixture_runner::DriveContext; +pub use crate::fixture_runner::FixtureCycleResult; +pub use crate::fixture_runner::FixtureRunner; +pub use crate::fixture_runner::InputPortHandle; +pub use crate::fixture_runner::ObserveContext; +pub use crate::fixture_runner::OutputPortHandle; +pub use crate::fixture_runner::PortBindings; +#[cfg(feature = "irvals")] pub use crate::irvals::cycles_from_irvals_file; +#[cfg(not(feature = "irvals"))] +pub fn cycles_from_irvals_file( + _m: &crate::pipeline_compile::CompiledPipelineModule, + _path: &std::path::Path, + _cycles_override: Option, +) -> Result> { + Err(Error::Parse( + "irvals support is disabled; build xlsynth-vastly with the `irvals` feature".to_string(), + )) +} pub use crate::iverilog_combo::run_iverilog_combo_and_collect_vcd; pub use crate::module_eval::step_module_with_env; pub use crate::parser_spanned::parse_expr_spanned; @@ -75,6 +96,7 @@ pub use crate::pipeline_compile::FunctionArmMeta; pub use crate::pipeline_compile::FunctionMeta; pub use crate::pipeline_compile::compile_pipeline_module; pub use crate::pipeline_compile::compile_pipeline_module_with_defines; +pub use crate::pipeline_compile::compile_pipeline_module_without_spans; pub use crate::pipeline_harness::PipelineCycle; pub use crate::pipeline_harness::PipelineStimulus; pub use crate::pipeline_harness::run_pipeline_and_collect_coverage; @@ -123,8 +145,12 @@ impl Env { } } - pub fn insert>(&mut self, name: S, value: Value4) { - self.vars.insert(name.into(), value); + pub fn insert>(&mut self, name: S, value: Value4) -> Option { + self.vars.insert(name.into(), value) + } + + pub fn remove(&mut self, name: &str) -> Option { + self.vars.remove(name) } pub fn get(&self, name: &str) -> Option<&Value4> { diff --git a/xlsynth-vastly/src/pipeline_compile.rs b/xlsynth-vastly/src/pipeline_compile.rs index 177deb56..afd84cff 100644 --- a/xlsynth-vastly/src/pipeline_compile.rs +++ b/xlsynth-vastly/src/pipeline_compile.rs @@ -66,21 +66,28 @@ impl CompiledPipelineModule { } pub fn compile_pipeline_module(src: &str) -> Result { - compile_pipeline_module_with_defines(src, &BTreeSet::new()) + compile_pipeline_module_with_options(src, &BTreeSet::new(), true) +} + +pub fn compile_pipeline_module_without_spans(src: &str) -> Result { + compile_pipeline_module_with_options(src, &BTreeSet::new(), false) } pub fn compile_pipeline_module_with_defines( src: &str, defines: &BTreeSet, ) -> Result { - let parse_src = src; - let parsed: ParsedModule = - crate::sv_parser::parse_pipeline_module_with_defines(parse_src, defines)?; - let items = crate::generate_constructs::elaborate_pipeline_items( - parse_src, - &parsed.params, - &parsed.items, - )?; + compile_pipeline_module_with_options(src, defines, true) +} + +fn compile_pipeline_module_with_options( + src: &str, + defines: &BTreeSet, + preserve_spans: bool, +) -> Result { + let parsed: ParsedModule = crate::sv_parser::parse_pipeline_module_with_defines(src, defines)?; + let items = + crate::generate_constructs::elaborate_pipeline_items(src, &parsed.params, &parsed.items)?; let module_name = parsed.name.clone(); @@ -102,14 +109,19 @@ pub fn compile_pipeline_module_with_defines( match it { ModuleItem::Decl { .. } => {} ModuleItem::Assign { - lhs, rhs, rhs_text, .. + lhs, + rhs, + rhs_spanned, + rhs_span, + .. } => { assigns.push(crate::compiled_module::lower_assign( - parse_src, lhs, - *rhs, - rhs_text.as_deref(), + rhs, + rhs_spanned, + *rhs_span, &decls, + preserve_spans, )?); } ModuleItem::Function { @@ -130,7 +142,7 @@ pub fn compile_pipeline_module_with_defines( } functions.insert( f.name.clone(), - crate::compiled_module::lower_function(parse_src, f, &decls)?, + crate::compiled_module::lower_function(src, f, &decls, preserve_spans)?, ); let mut arms_meta: Vec = Vec::new(); @@ -150,14 +162,14 @@ pub fn compile_pipeline_module_with_defines( for a in arms { arms_meta.push(FunctionArmMeta { arm_span: a.arm_span, - value_span: a.value, + value_span: a.value_span, }); } } - crate::sv_ast::FunctionBody::Assign { value } => { + crate::sv_ast::FunctionBody::Assign { value_span, .. } => { scaffold_spans.push(*begin_span); scaffold_spans.push(*end_span); - assign_expr_span = Some(*value); + assign_expr_span = Some(*value_span); } crate::sv_ast::FunctionBody::Procedure { .. } => { scaffold_spans.push(*begin_span); diff --git a/xlsynth-vastly/src/pipeline_harness.rs b/xlsynth-vastly/src/pipeline_harness.rs index bd9b6ad3..c1088b0e 100644 --- a/xlsynth-vastly/src/pipeline_harness.rs +++ b/xlsynth-vastly/src/pipeline_harness.rs @@ -154,7 +154,7 @@ pub fn run_pipeline_and_collect_coverage( return Err(Error::Parse("no cycles provided".to_string())); } let plan = plan_combo_eval(&m.combo)?; - let skip_toggle_names = literal_assigned_names(m); + let skip_toggle_names = literal_assigned_names(m)?; let mut state = initial_state.clone(); let mut last_values: BTreeMap = BTreeMap::new(); @@ -401,10 +401,18 @@ fn update_toggles( } } -fn literal_assigned_names(m: &crate::pipeline_compile::CompiledPipelineModule) -> BTreeSet { +fn literal_assigned_names( + m: &crate::pipeline_compile::CompiledPipelineModule, +) -> Result> { let mut s: BTreeSet = BTreeSet::new(); for a in &m.combo.assigns { - match &a.rhs_spanned.kind { + let rhs_spanned = a.rhs_spanned.as_ref().ok_or_else(|| { + Error::Parse( + "coverage requires a module compiled with spans; use compile_pipeline_module or compile_pipeline_module_with_defines" + .to_string(), + ) + })?; + match &rhs_spanned.kind { crate::ast_spanned::SpannedExprKind::Literal(_) => { s.insert(a.lhs_base().to_string()); } @@ -417,5 +425,5 @@ fn literal_assigned_names(m: &crate::pipeline_compile::CompiledPipelineModule) - _ => {} } } - s + Ok(s) } diff --git a/xlsynth-vastly/src/sv_ast.rs b/xlsynth-vastly/src/sv_ast.rs index facf8a3a..f619fdd4 100644 --- a/xlsynth-vastly/src/sv_ast.rs +++ b/xlsynth-vastly/src/sv_ast.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::ast::Expr as VExpr; +use crate::ast_spanned::SpannedExpr; use crate::value::Value4; use std::collections::BTreeMap; @@ -57,12 +58,14 @@ pub struct FunctionDecl { pub enum FunctionBody { UniqueCasez { casez_span: Span, - selector: Span, + selector: VExpr, + selector_span: Span, endcase_span: Span, arms: Vec, }, Assign { - value: Span, + value: VExpr, + value_span: Span, }, Procedure { assigns: Vec, @@ -72,7 +75,8 @@ pub enum FunctionBody { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FunctionAssign { pub lhs: String, - pub value: Span, + pub value: VExpr, + pub value_span: Span, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -88,7 +92,8 @@ pub struct CasezArm { pub pat: Option, // None => default pub pat_span: Option, pub arm_span: Span, - pub value: Span, + pub value: VExpr, + pub value_span: Span, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -114,8 +119,9 @@ pub enum ModuleItem { }, Assign { lhs: Lhs, - rhs: Span, - rhs_text: Option, + rhs: VExpr, + rhs_spanned: SpannedExpr, + rhs_span: Span, span: Span, }, Function { diff --git a/xlsynth-vastly/src/sv_parser.rs b/xlsynth-vastly/src/sv_parser.rs index f0e2f039..93fd4479 100644 --- a/xlsynth-vastly/src/sv_parser.rs +++ b/xlsynth-vastly/src/sv_parser.rs @@ -7,6 +7,7 @@ use crate::Value4; use crate::ast::Expr as VExpr; use crate::eval::eval_ast_with_calls; use crate::parser::parse_expr; +use crate::parser_spanned::parse_expr_spanned; use crate::sv_ast::AlwaysFf; use crate::sv_ast::CasezArm; use crate::sv_ast::CasezPattern; @@ -627,13 +628,15 @@ impl<'a> Parser<'a> { self.expect(TokKind::KwAssign)?; let lhs = self.parse_lhs()?; self.expect(TokKind::Eq)?; - let rhs = self.parse_span_until_semi()?; + let (rhs, rhs_spanned, rhs_span) = + self.parse_expr_and_spanned_with_span_until_semi()?; self.expect(TokKind::Semi)?; let stmt_end = self.toks[self.idx - 1].end; Ok(ModuleItem::Assign { lhs, rhs, - rhs_text: None, + rhs_spanned, + rhs_span, span: Span { start: stmt_start, end: stmt_end, @@ -1013,13 +1016,14 @@ impl<'a> Parser<'a> { self.expect(TokKind::KwAssign)?; let lhs = self.parse_lhs()?; self.expect(TokKind::Eq)?; - let rhs = self.parse_span_until_semi()?; + let (rhs, rhs_spanned, rhs_span) = self.parse_expr_and_spanned_with_span_until_semi()?; self.expect(TokKind::Semi)?; let stmt_end = self.toks[self.idx - 1].end; Ok(ModuleItem::Assign { lhs, rhs, - rhs_text: None, + rhs_spanned, + rhs_span, span: Span { start: stmt_start, end: stmt_end, @@ -1089,7 +1093,8 @@ impl<'a> Parser<'a> { let assigns = self.parse_procedural_function_body()?; if assigns.len() == 1 && assigns[0].lhs == name { FunctionBody::Assign { - value: assigns[0].value, + value: assigns[0].value.clone(), + value_span: assigns[0].value_span, } } else { FunctionBody::Procedure { assigns } @@ -1148,9 +1153,13 @@ impl<'a> Parser<'a> { } }; self.expect(TokKind::Eq)?; - let value = self.parse_span_until_semi()?; + let (value, value_span) = self.parse_expr_with_span_until_semi()?; self.expect(TokKind::Semi)?; - assigns.push(FunctionAssign { lhs, value }); + assigns.push(FunctionAssign { + lhs, + value, + value_span, + }); } Ok(assigns) } @@ -1234,7 +1243,7 @@ impl<'a> Parser<'a> { } self.expect(TokKind::KwCasez)?; self.expect(TokKind::LParen)?; - let selector = self.parse_span_until(&[TokKind::RParen])?; + let (selector, selector_span) = self.parse_expr_with_span_until(&[TokKind::RParen])?; self.expect(TokKind::RParen)?; let casez_end = self.toks[self.idx - 1].end; let casez_span = Span { @@ -1318,7 +1327,7 @@ impl<'a> Parser<'a> { ))); } self.expect(TokKind::Eq)?; - let value = self.parse_span_until_semi()?; + let (value, value_span) = self.parse_expr_with_span_until_semi()?; self.expect(TokKind::Semi)?; self.expect(TokKind::KwEnd)?; let arm_end = self.toks[self.idx - 1].end; @@ -1331,11 +1340,13 @@ impl<'a> Parser<'a> { end: arm_end, }, value, + value_span, }); }; Ok(FunctionBody::UniqueCasez { casez_span, selector, + selector_span, endcase_span, arms, }) @@ -1535,38 +1546,54 @@ impl<'a> Parser<'a> { } fn parse_expr_until(&mut self, end_kinds: &[TokKind]) -> Result { - let start = self.toks[self.idx].start; - let mut depth_paren: i32 = 0; - let mut depth_brace: i32 = 0; - let mut depth_bracket: i32 = 0; + self.parse_expr_with_span_until(end_kinds) + .map(|(expr, _)| expr) + } - while self.idx < self.toks.len() { - let k = self.toks[self.idx].kind.clone(); - if depth_paren == 0 - && depth_brace == 0 - && depth_bracket == 0 - && end_kinds.iter().any(|x| *x == k) - { - break; - } - match &k { - TokKind::LParen => depth_paren += 1, - TokKind::RParen => depth_paren -= 1, - TokKind::LBracket => depth_bracket += 1, - TokKind::RBracket => depth_bracket -= 1, - TokKind::Other('{') => depth_brace += 1, - TokKind::Other('}') => depth_brace -= 1, - _ => {} - } - self.idx += 1; + fn parse_expr_with_span_until(&mut self, end_kinds: &[TokKind]) -> Result<(VExpr, Span)> { + let span = self.parse_span_until(end_kinds)?; + self.parse_expr_in_span(span) + } + + fn parse_expr_with_span_until_semi(&mut self) -> Result<(VExpr, Span)> { + self.parse_expr_with_span_until(&[TokKind::Semi]) + } + + fn parse_expr_and_spanned_with_span_until_semi( + &mut self, + ) -> Result<(VExpr, crate::ast_spanned::SpannedExpr, Span)> { + self.parse_expr_and_spanned_with_span_until(&[TokKind::Semi]) + } + + fn parse_expr_and_spanned_with_span_until( + &mut self, + end_kinds: &[TokKind], + ) -> Result<(VExpr, crate::ast_spanned::SpannedExpr, Span)> { + let span = self.parse_span_until(end_kinds)?; + self.parse_expr_and_spanned_in_span(span) + } + + fn parse_expr_in_span(&self, span: Span) -> Result<(VExpr, Span)> { + let slice = self.src[span.start..span.end].trim(); + if slice.is_empty() { + return Err(Error::Parse("empty expression".to_string())); } + let expr = parse_expr(slice)?; + Ok((expr, span)) + } - let end = self.toks[self.idx].start; - let slice = self.src[start..end].trim(); + fn parse_expr_and_spanned_in_span( + &self, + span: Span, + ) -> Result<(VExpr, crate::ast_spanned::SpannedExpr, Span)> { + let slice = self.src[span.start..span.end].trim(); if slice.is_empty() { return Err(Error::Parse("empty expression".to_string())); } - parse_expr(slice) + let expr = parse_expr(slice)?; + let mut spanned = parse_expr_spanned(slice)?; + spanned.shift_spans(span.start); + Ok((expr, spanned, span)) } fn parse_span_until(&mut self, end_kinds: &[TokKind]) -> Result { @@ -1603,10 +1630,6 @@ impl<'a> Parser<'a> { Ok(Span { start, end }) } - fn parse_span_until_semi(&mut self) -> Result { - self.parse_span_until(&[TokKind::Semi]) - } - // no skip_balanced_parens in v1; we parse a restricted port list. } diff --git a/xlsynth-vastly/src/value.rs b/xlsynth-vastly/src/value.rs index 2e7b2ac0..e77a6bd1 100644 --- a/xlsynth-vastly/src/value.rs +++ b/xlsynth-vastly/src/value.rs @@ -66,6 +66,24 @@ impl Value4 { } } + pub fn from_u64(width: u32, signedness: Signedness, value: u64) -> Self { + let mut bits = Vec::with_capacity(width as usize); + for bit in 0..width { + bits.push(if bit >= 64 { + LogicBit::Zero + } else if value & (1u64 << bit) == 0 { + LogicBit::Zero + } else { + LogicBit::One + }); + } + Self { + width, + signedness, + bits, + } + } + pub fn from_bits_msb_first( width: u32, signedness: Signedness, @@ -192,6 +210,65 @@ impl Value4 { Some(out) } + pub fn to_u64_if_known(&self) -> Option { + if self.width > 64 || !self.is_all_known_01() { + return None; + } + let mut out = 0u64; + for bit in 0..self.width { + if self.bit(bit) == LogicBit::One { + out |= 1u64 << bit; + } + } + Some(out) + } + + pub fn slice_lsb_width(&self, lsb: u32, width: u32) -> crate::Result { + let end = lsb + .checked_add(width) + .ok_or_else(|| crate::Error::Parse("bit slice overflow".to_string()))?; + if width == 0 { + return Err(crate::Error::Parse( + "bit slice width must be > 0".to_string(), + )); + } + if end > self.width { + return Err(crate::Error::Parse(format!( + "bit slice {}..{} is out of bounds for width {}", + lsb, end, self.width + ))); + } + Ok(Self { + width, + signedness: self.signedness, + bits: self.bits[lsb as usize..end as usize].to_vec(), + }) + } + + pub fn replace_slice(&self, lsb: u32, value: &Value4) -> crate::Result { + let end = lsb + .checked_add(value.width) + .ok_or_else(|| crate::Error::Parse("bit slice overflow".to_string()))?; + if value.width == 0 { + return Err(crate::Error::Parse( + "replacement slice width must be > 0".to_string(), + )); + } + if end > self.width { + return Err(crate::Error::Parse(format!( + "replacement slice {}..{} is out of bounds for width {}", + lsb, end, self.width + ))); + } + let mut bits = self.bits.clone(); + bits[lsb as usize..end as usize].copy_from_slice(value.bits_lsb_first()); + Ok(Self { + width: self.width, + signedness: self.signedness, + bits, + }) + } + pub fn parse_numeric_token( width: u32, signedness: Signedness, @@ -268,41 +345,40 @@ impl Value4 { } pub fn resize(&self, new_width: u32) -> Self { + self.clone().into_width(new_width) + } + + pub fn with_signedness(&self, signedness: Signedness) -> Self { + self.clone().into_signedness(signedness) + } + + pub fn into_width(mut self, new_width: u32) -> Self { if new_width == self.width { - return self.clone(); + return self; } if new_width < self.width { - let mut bits = self.bits.clone(); - bits.truncate(new_width as usize); - return Self { - width: new_width, - signedness: self.signedness, - bits, - }; + self.bits.truncate(new_width as usize); + self.width = new_width; + return self; } - let mut bits = self.bits.clone(); let ext_bit = match self.signedness { Signedness::Unsigned => LogicBit::Zero, + Signedness::Signed if self.width == 0 => LogicBit::Zero, Signedness::Signed => self.msb(), }; - bits.extend(std::iter::repeat(ext_bit).take((new_width - self.width) as usize)); - Self { - width: new_width, - signedness: self.signedness, - bits, - } + self.bits.resize(new_width as usize, ext_bit); + self.width = new_width; + self } - pub fn with_signedness(&self, signedness: Signedness) -> Self { - if self.signedness == signedness { - return self.clone(); - } - Self { - width: self.width, - signedness, - bits: self.bits.clone(), - } + pub fn into_signedness(mut self, signedness: Signedness) -> Self { + self.signedness = signedness; + self + } + + pub fn into_width_and_signedness(self, new_width: u32, signedness: Signedness) -> Self { + self.into_signedness(signedness).into_width(new_width) } pub fn bitwise_not(&self) -> Self { @@ -325,8 +401,8 @@ impl Value4 { pub fn bitwise_and(&self, rhs: &Value4) -> Value4 { let w = self.width.max(rhs.width); - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); let mut bits = Vec::with_capacity(w as usize); for i in 0..w { bits.push(bit_and_4(a.bit(i), b.bit(i))); @@ -336,8 +412,8 @@ impl Value4 { pub fn bitwise_or(&self, rhs: &Value4) -> Value4 { let w = self.width.max(rhs.width); - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); let mut bits = Vec::with_capacity(w as usize); for i in 0..w { bits.push(bit_or_4(a.bit(i), b.bit(i))); @@ -347,8 +423,8 @@ impl Value4 { pub fn bitwise_xor(&self, rhs: &Value4) -> Value4 { let w = self.width.max(rhs.width); - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); let mut bits = Vec::with_capacity(w as usize); for i in 0..w { bits.push(bit_xor_4(a.bit(i), b.bit(i))); @@ -362,8 +438,8 @@ impl Value4 { if self.has_unknown() || rhs.has_unknown() { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); let bits = add_known_bits_lsb(a.bits_lsb_first(), b.bits_lsb_first()); Value4::new(w, signedness, bits) } @@ -374,8 +450,8 @@ impl Value4 { if self.has_unknown() || rhs.has_unknown() { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); let bits = sub_known_bits_lsb(a.bits_lsb_first(), b.bits_lsb_first()); Value4::new(w, signedness, bits) } @@ -386,8 +462,38 @@ impl Value4 { if self.has_unknown() || rhs.has_unknown() { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } - let a = self.resize(w); - let b = rhs.resize(w); + let a = Value4Ref::resize(self, w); + let b = Value4Ref::resize(rhs, w); + + if is_all_zero_known_bits(a.bits_lsb_first()) || is_all_zero_known_bits(b.bits_lsb_first()) + { + return Value4::zeros(w, signedness); + } + if is_known_one_bits(a.bits_lsb_first()) { + return b.with_signedness(signedness); + } + if is_known_one_bits(b.bits_lsb_first()) { + return a.with_signedness(signedness); + } + if is_all_one_known_bits(a.bits_lsb_first()) { + return b.unary_minus().with_signedness(signedness); + } + if is_all_one_known_bits(b.bits_lsb_first()) { + return a.unary_minus().with_signedness(signedness); + } + if let Some(shift) = known_single_bit_index(a.bits_lsb_first()) { + return shl_known_bits(a.width, signedness, b.bits_lsb_first(), shift); + } + if let Some(shift) = known_single_bit_index(b.bits_lsb_first()) { + return shl_known_bits(a.width, signedness, a.bits_lsb_first(), shift); + } + if w <= 128 { + let a_bits = known_bits_to_u128(a.bits_lsb_first()); + let b_bits = known_bits_to_u128(b.bits_lsb_first()); + let product = a_bits.wrapping_mul(b_bits) & mask_for_width(w); + return Value4::new(w, signedness, u128_to_known_bits_lsb(product, w as usize)); + } + let bits = mul_known_bits_lsb(a.bits_lsb_first(), b.bits_lsb_first(), w as usize); Value4::new(w, signedness, bits) } @@ -398,8 +504,8 @@ impl Value4 { if self.has_unknown() || rhs.has_unknown() { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } - let numer = self.resize(w); - let denom = rhs.resize(w); + let numer = Value4Ref::resize(self, w); + let denom = Value4Ref::resize(rhs, w); if is_all_zero_known_bits(denom.bits_lsb_first()) { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } @@ -434,8 +540,8 @@ impl Value4 { if self.has_unknown() || rhs.has_unknown() { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } - let numer = self.resize(w); - let denom = rhs.resize(w); + let numer = Value4Ref::resize(self, w); + let denom = Value4Ref::resize(rhs, w); if is_all_zero_known_bits(denom.bits_lsb_first()) { return Value4::new(w, signedness, vec![LogicBit::X; w as usize]); } @@ -553,11 +659,11 @@ impl Value4 { let use_signed = self.signedness == Signedness::Signed && rhs.signedness == Signedness::Signed; let (a, b) = if use_signed { - (self.resize(w), rhs.resize(w)) + (Value4Ref::resize(self, w), Value4Ref::resize(rhs, w)) } else { ( - self.with_signedness(Signedness::Unsigned).resize(w), - rhs.with_signedness(Signedness::Unsigned).resize(w), + Value4Ref::recontext(self, w, Signedness::Unsigned), + Value4Ref::recontext(rhs, w, Signedness::Unsigned), ) }; let ord = if use_signed { @@ -627,11 +733,11 @@ impl Value4 { let use_signed = self.signedness == Signedness::Signed && rhs.signedness == Signedness::Signed; let (a, b) = if use_signed { - (self.resize(w), rhs.resize(w)) + (Value4Ref::resize(self, w), Value4Ref::resize(rhs, w)) } else { ( - self.with_signedness(Signedness::Unsigned).resize(w), - rhs.with_signedness(Signedness::Unsigned).resize(w), + Value4Ref::recontext(self, w, Signedness::Unsigned), + Value4Ref::recontext(rhs, w, Signedness::Unsigned), ) }; @@ -668,11 +774,11 @@ impl Value4 { let use_signed = self.signedness == Signedness::Signed && rhs.signedness == Signedness::Signed; let (a, b) = if use_signed { - (self.resize(w), rhs.resize(w)) + (Value4Ref::resize(self, w), Value4Ref::resize(rhs, w)) } else { ( - self.with_signedness(Signedness::Unsigned).resize(w), - rhs.with_signedness(Signedness::Unsigned).resize(w), + Value4Ref::recontext(self, w, Signedness::Unsigned), + Value4Ref::recontext(rhs, w, Signedness::Unsigned), ) }; @@ -702,11 +808,11 @@ impl Value4 { } else { Signedness::Unsigned }; - let t2 = t.with_signedness(out_signedness).resize(out_width); - let f2 = f.with_signedness(out_signedness).resize(out_width); + let t2 = Value4Ref::recontext(t, out_width, out_signedness); + let f2 = Value4Ref::recontext(f, out_width, out_signedness); match c { - LogicBit::One => t2, - LogicBit::Zero => f2, + LogicBit::One => t2.into_owned(), + LogicBit::Zero => f2.into_owned(), LogicBit::X | LogicBit::Z => { let mut bits = Vec::with_capacity(out_width as usize); for i in 0..out_width { @@ -915,6 +1021,47 @@ fn logic_bit_from_bool(v: bool) -> LogicBit { if v { LogicBit::One } else { LogicBit::Zero } } +enum Value4Ref<'a> { + Borrowed(&'a Value4), + Owned(Value4), +} + +impl<'a> Value4Ref<'a> { + fn resize(value: &'a Value4, width: u32) -> Self { + if value.width == width { + Self::Borrowed(value) + } else { + Self::Owned(value.clone().into_width(width)) + } + } + + fn recontext(value: &'a Value4, width: u32, signedness: Signedness) -> Self { + if value.width == width && value.signedness == signedness { + Self::Borrowed(value) + } else { + Self::Owned(value.clone().into_width_and_signedness(width, signedness)) + } + } + + fn into_owned(self) -> Value4 { + match self { + Self::Borrowed(value) => value.clone(), + Self::Owned(value) => value, + } + } +} + +impl std::ops::Deref for Value4Ref<'_> { + type Target = Value4; + + fn deref(&self) -> &Self::Target { + match self { + Self::Borrowed(value) => value, + Self::Owned(value) => value, + } + } +} + fn cmp_known_bits_unsigned_lsb(a: &[LogicBit], b: &[LogicBit]) -> Ordering { debug_assert_eq!(a.len(), b.len()); debug_assert!(a.iter().all(|bit| bit.is_known_01())); @@ -1030,6 +1177,79 @@ fn is_all_zero_known_bits(bits: &[LogicBit]) -> bool { bits.iter().all(|bit| *bit == LogicBit::Zero) } +fn is_all_one_known_bits(bits: &[LogicBit]) -> bool { + debug_assert!(bits.iter().all(|bit| bit.is_known_01())); + bits.iter().all(|bit| *bit == LogicBit::One) +} + +fn is_known_one_bits(bits: &[LogicBit]) -> bool { + debug_assert!(bits.iter().all(|bit| bit.is_known_01())); + bits.first() == Some(&LogicBit::One) && bits[1..].iter().all(|bit| *bit == LogicBit::Zero) +} + +fn known_single_bit_index(bits: &[LogicBit]) -> Option { + debug_assert!(bits.iter().all(|bit| bit.is_known_01())); + let mut found: Option = None; + for (idx, bit) in bits.iter().enumerate() { + if *bit != LogicBit::One { + continue; + } + let idx = idx as u32; + if found.is_some() { + return None; + } + found = Some(idx); + } + found +} + +fn shl_known_bits(width: u32, signedness: Signedness, bits: &[LogicBit], shift: u32) -> Value4 { + debug_assert_eq!(bits.len(), width as usize); + debug_assert!(bits.iter().all(|bit| bit.is_known_01())); + if shift >= width { + return Value4::zeros(width, signedness); + } + let mut out = vec![LogicBit::Zero; width as usize]; + for src in 0..(width - shift) as usize { + out[src + shift as usize] = bits[src]; + } + Value4::new(width, signedness, out) +} + +fn known_bits_to_u128(bits: &[LogicBit]) -> u128 { + debug_assert!(bits.len() <= 128); + debug_assert!(bits.iter().all(|bit| bit.is_known_01())); + let mut out = 0u128; + for (idx, bit) in bits.iter().enumerate() { + if *bit == LogicBit::One { + out |= 1u128 << idx; + } + } + out +} + +fn u128_to_known_bits_lsb(value: u128, width: usize) -> Vec { + let mut out = Vec::with_capacity(width); + for idx in 0..width { + out.push(if (value >> idx) & 1 == 0 { + LogicBit::Zero + } else { + LogicBit::One + }); + } + out +} + +fn mask_for_width(width: u32) -> u128 { + if width >= 128 { + u128::MAX + } else if width == 0 { + 0 + } else { + (1u128 << width) - 1 + } +} + fn div_mod_known_bits_lsb( numer: &[LogicBit], denom: &[LogicBit], @@ -1302,6 +1522,19 @@ mod tests { assert_eq!(a.mul(&b), ubits(136, &[0, 89, 128])); } + #[test] + fn mul_width_128_wraps_with_native_fast_path() { + let all_ones = Value4::new(128, Signedness::Unsigned, vec![LogicBit::One; 128]); + let two = ubits(128, &[1]); + let mut expected_bits = vec![LogicBit::One; 128]; + expected_bits[0] = LogicBit::Zero; + + assert_eq!( + all_ones.mul(&two), + Value4::new(128, Signedness::Unsigned, expected_bits) + ); + } + #[test] fn wide_known_to_u32_conversion_uses_low_bits_and_saturates() { assert_eq!(ubits(140, &[5]).to_u32_saturating_if_known(), Some(32)); diff --git a/xlsynth-vastly/tests/coverage_pipeline.rs b/xlsynth-vastly/tests/coverage_pipeline.rs index af9d0845..5b432c33 100644 --- a/xlsynth-vastly/tests/coverage_pipeline.rs +++ b/xlsynth-vastly/tests/coverage_pipeline.rs @@ -11,6 +11,7 @@ use xlsynth_vastly::SourceText; use xlsynth_vastly::Value4; use xlsynth_vastly::compile_pipeline_module; use xlsynth_vastly::compile_pipeline_module_with_defines; +use xlsynth_vastly::compile_pipeline_module_without_spans; use xlsynth_vastly::run_pipeline_and_collect_coverage; use xlsynth_vastly::run_pipeline_and_write_vcd; @@ -65,7 +66,11 @@ fn collects_line_ternary_and_toggle_coverage() { let mut cov = CoverageCounters::default(); for a in &cm.combo.assigns { - cov.register_ternaries_from_spanned_expr(&a.rhs_spanned); + cov.register_ternaries_from_spanned_expr( + a.rhs_spanned + .as_ref() + .expect("coverage registration requires spanned assign expressions"), + ); } run_pipeline_and_collect_coverage(&cm, &stimulus, &init, &src, &mut cov).unwrap(); @@ -213,3 +218,31 @@ fn coverage_attributes_stateful_always_ff_lines() { assert!(cov.line_hits.get(&7).copied().unwrap_or(0) > 0); assert!(cov.line_hits.get(&8).copied().unwrap_or(0) > 0); } + +#[test] +fn coverage_rejects_modules_compiled_without_spans() { + let dut = concat!( + "module m(\n", + " input logic clk,\n", + " input logic a,\n", + " output wire y\n", + ");\n", + " assign y = a;\n", + "endmodule\n", + ); + let cm = compile_pipeline_module_without_spans(dut).unwrap(); + let src = SourceText::new(dut.to_string()); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![PipelineCycle { + inputs: [("a".to_string(), vbits(1, Signedness::Unsigned, "0"))] + .into_iter() + .collect(), + }], + }; + let init = BTreeMap::new(); + let mut cov = CoverageCounters::default(); + + let err = run_pipeline_and_collect_coverage(&cm, &stimulus, &init, &src, &mut cov).unwrap_err(); + assert!(format!("{err:?}").contains("coverage requires a module compiled with spans")); +} diff --git a/xlsynth-vastly/tests/fixture_runner.rs b/xlsynth-vastly/tests/fixture_runner.rs new file mode 100644 index 00000000..78479889 --- /dev/null +++ b/xlsynth-vastly/tests/fixture_runner.rs @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: Apache-2.0 + +use xlsynth_vastly::CycleFixture; +use xlsynth_vastly::DriveContext; +use xlsynth_vastly::FixtureRunner; +use xlsynth_vastly::ObserveContext; +use xlsynth_vastly::compile_pipeline_module; + +#[test] +fn port_binding_rejects_missing_or_wrong_width() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [7:0] data_in, + output logic done +); + assign done = rst; +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let runner = FixtureRunner::new(&module).unwrap(); + let ports = runner.port_bindings(); + + let err = ports.input("missing").unwrap_err(); + assert!(format!("{err:?}").contains("input port `missing` was not found")); + + let err = ports.input("clk").unwrap_err(); + assert!( + format!("{err:?}") + .contains("clock `clk` is managed internally and is not exposed as a fixture input") + ); + + let err = ports.output_with_width("done", 2).unwrap_err(); + assert!(format!("{err:?}").contains("output port `done` has width 1, expected 2")); +} + +#[test] +fn fixture_runner_rejects_clock_dependent_outputs() { + let dut = r#" +module m( + input logic clk, + output logic observed +); + function automatic logic helper(input logic enable); + begin + helper = enable & clk; + end + endfunction + + assign observed = helper(1'b1); +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let err = match FixtureRunner::new(&module) { + Ok(_) => panic!("expected clock-sensitive outputs to be rejected"), + Err(err) => err, + }; + assert!(format!("{err:?}").contains( + "fixture runner does not support combinational outputs that depend on clock `clk`: observed" + )); +} + +#[test] +fn fixture_runner_can_drive_and_sample_packed_slices() { + struct PackedFixture { + in_bus: Option, + out_bus: Option, + observed: Vec<(u64, u64)>, + } + + impl CycleFixture for PackedFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.in_bus = Some(ports.input_with_width("in_bus", 32)?); + self.out_bus = Some(ports.output_with_width("out_bus", 32)?); + Ok(()) + } + + fn drive_cycle_inputs( + &mut self, + cycle: u64, + ctx: &mut DriveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + if cycle == 0 { + let in_bus = self.in_bus.as_ref().unwrap(); + ctx.set_packed_u64(in_bus, &[0], 0x11)?; + ctx.set_packed_u64(in_bus, &[2], 0xab)?; + } + Ok(()) + } + + fn observe_cycle_outputs( + &mut self, + cycle: u64, + ctx: &ObserveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + let out_bus = self.out_bus.as_ref().unwrap(); + let lane0 = ctx.output_packed_u64_if_known(out_bus, &[0])?.unwrap(); + let lane2 = ctx.output_packed_u64_if_known(out_bus, &[2])?.unwrap(); + self.observed.push((cycle, (lane2 << 8) | lane0)); + Ok(()) + } + } + + let dut = r#" +module m( + input logic clk, + input logic [3:0][7:0] in_bus, + output logic [3:0][7:0] out_bus +); + assign out_bus = in_bus; +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let mut runner = FixtureRunner::new(&module).unwrap(); + let mut fixture = PackedFixture { + in_bus: None, + out_bus: None, + observed: Vec::new(), + }; + runner.bind_fixture(&mut fixture).unwrap(); + let mut fixtures: Vec<&mut dyn CycleFixture> = vec![&mut fixture]; + runner.step_cycle(&mut fixtures).unwrap(); + + assert_eq!(fixture.observed, vec![(0, 0xab11)]); +} + +#[test] +fn fixture_runner_handles_next_cycle_response_with_multiple_fixtures() { + struct LaunchFixture { + launch: Option, + payload: Option, + } + + impl CycleFixture for LaunchFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.launch = Some(ports.input_with_width("launch", 1)?); + self.payload = Some(ports.input_with_width("launch_data", 8)?); + Ok(()) + } + + fn drive_cycle_inputs( + &mut self, + cycle: u64, + ctx: &mut DriveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + if cycle == 1 { + ctx.set_bool(self.launch.as_ref().unwrap(), true)?; + ctx.set_u64(self.payload.as_ref().unwrap(), 0x3c)?; + } + Ok(()) + } + } + + struct ResponderFixture { + req_valid: Option, + req_data: Option, + resp_valid: Option, + resp_data: Option, + scheduled_response: Option, + } + + impl CycleFixture for ResponderFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.req_valid = Some(ports.output_with_width("req_valid", 1)?); + self.req_data = Some(ports.output_with_width("req_data", 8)?); + self.resp_valid = Some(ports.input_with_width("resp_valid", 1)?); + self.resp_data = Some(ports.input_with_width("resp_data", 8)?); + Ok(()) + } + + fn drive_cycle_inputs( + &mut self, + _cycle: u64, + ctx: &mut DriveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + if let Some(value) = self.scheduled_response.take() { + ctx.set_bool(self.resp_valid.as_ref().unwrap(), true)?; + ctx.set_u64(self.resp_data.as_ref().unwrap(), value)?; + } + Ok(()) + } + + fn observe_cycle_outputs( + &mut self, + _cycle: u64, + ctx: &ObserveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + if ctx + .output_bool_if_known(self.req_valid.as_ref().unwrap())? + .unwrap_or(false) + { + let req_data = ctx + .output_u64_if_known(self.req_data.as_ref().unwrap())? + .unwrap(); + self.scheduled_response = Some(req_data + 1); + } + Ok(()) + } + } + + struct ScoreFixture { + done: Option, + result: Option, + observed_results: Vec, + } + + impl CycleFixture for ScoreFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.done = Some(ports.output_with_width("done", 1)?); + self.result = Some(ports.output_with_width("result", 8)?); + Ok(()) + } + + fn observe_cycle_outputs( + &mut self, + _cycle: u64, + ctx: &ObserveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + if ctx + .output_bool_if_known(self.done.as_ref().unwrap())? + .unwrap_or(false) + { + self.observed_results.push( + ctx.output_u64_if_known(self.result.as_ref().unwrap())? + .unwrap(), + ); + } + Ok(()) + } + } + + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic launch, + input logic [7:0] launch_data, + output logic req_valid, + output logic [7:0] req_data, + input logic resp_valid, + input logic [7:0] resp_data, + output logic done, + output logic [7:0] result +); + logic pending; + logic [7:0] req_data_q; + logic [7:0] result_q; + + always_ff @(posedge clk) begin + if (rst) begin + pending <= 1'b0; + req_data_q <= 8'h00; + result_q <= 8'h00; + end else begin + if (launch) begin + pending <= 1'b1; + req_data_q <= launch_data; + end else if (resp_valid) begin + pending <= 1'b0; + result_q <= resp_data; + end + end + end + + assign req_valid = pending; + assign req_data = req_data_q; + assign done = resp_valid; + assign result = result_q; +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let mut runner = FixtureRunner::new(&module).unwrap(); + let rst = runner.port_bindings().input_with_width("rst", 1).unwrap(); + + let mut launch = LaunchFixture { + launch: None, + payload: None, + }; + let mut responder = ResponderFixture { + req_valid: None, + req_data: None, + resp_valid: None, + resp_data: None, + scheduled_response: None, + }; + let mut score = ScoreFixture { + done: None, + result: None, + observed_results: Vec::new(), + }; + let mut fixtures: Vec<&mut dyn CycleFixture> = vec![&mut launch, &mut responder, &mut score]; + runner.bind_fixtures(&mut fixtures).unwrap(); + runner + .step_cycle_with_drive(&mut fixtures, |ctx| ctx.set_bool(&rst, true)) + .unwrap(); + for _ in 0..4 { + runner.step_cycle(&mut fixtures).unwrap(); + } + + assert_eq!(score.observed_results, vec![0x3d]); +} + +#[test] +fn fixture_runner_rejects_multiple_fixtures_driving_same_input() { + struct BoolDriverFixture { + handle: Option, + value: bool, + } + + impl CycleFixture for BoolDriverFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.handle = Some(ports.input_with_width("shared_in", 1)?); + Ok(()) + } + + fn drive_cycle_inputs( + &mut self, + _cycle: u64, + ctx: &mut DriveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + ctx.set_bool(self.handle.as_ref().unwrap(), self.value) + } + } + + let dut = r#" +module m( + input logic clk, + input logic shared_in, + output logic observed +); + assign observed = shared_in; +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let mut runner = FixtureRunner::new(&module).unwrap(); + let mut fixture0 = BoolDriverFixture { + handle: None, + value: false, + }; + let mut fixture1 = BoolDriverFixture { + handle: None, + value: true, + }; + let mut fixtures: Vec<&mut dyn CycleFixture> = vec![&mut fixture0, &mut fixture1]; + runner.bind_fixtures(&mut fixtures).unwrap(); + + let err = runner.step_cycle(&mut fixtures).unwrap_err(); + assert!( + format!("{err:?}") + .contains("input port `shared_in` was already driven earlier in the cycle") + ); +} + +#[test] +fn fixture_runner_rejects_overlapping_packed_slice_drives() { + struct PackedDriverFixture { + handle: Option, + lane: u32, + value: u64, + } + + impl CycleFixture for PackedDriverFixture { + fn bind(&mut self, ports: &xlsynth_vastly::PortBindings<'_>) -> xlsynth_vastly::Result<()> { + self.handle = Some(ports.input_with_width("shared_bus", 16)?); + Ok(()) + } + + fn drive_cycle_inputs( + &mut self, + _cycle: u64, + ctx: &mut DriveContext<'_>, + ) -> xlsynth_vastly::Result<()> { + ctx.set_packed_u64(self.handle.as_ref().unwrap(), &[self.lane], self.value) + } + } + + let dut = r#" +module m( + input logic clk, + input logic [1:0][7:0] shared_bus, + output logic [1:0][7:0] observed_bus +); + assign observed_bus = shared_bus; +endmodule +"#; + + let module = compile_pipeline_module(dut).unwrap(); + let mut runner = FixtureRunner::new(&module).unwrap(); + let mut fixture0 = PackedDriverFixture { + handle: None, + lane: 0, + value: 0x12, + }; + let mut fixture1 = PackedDriverFixture { + handle: None, + lane: 0, + value: 0x34, + }; + let mut fixtures: Vec<&mut dyn CycleFixture> = vec![&mut fixture0, &mut fixture1]; + runner.bind_fixtures(&mut fixtures).unwrap(); + + let err = runner.step_cycle(&mut fixtures).unwrap_err(); + assert!( + format!("{err:?}") + .contains("input port `shared_bus` slice [0] was already driven earlier in the cycle") + ); +} diff --git a/xlsynth-vastly/tests/generate_constructs.rs b/xlsynth-vastly/tests/generate_constructs.rs index 199ab547..243edd54 100644 --- a/xlsynth-vastly/tests/generate_constructs.rs +++ b/xlsynth-vastly/tests/generate_constructs.rs @@ -2,15 +2,19 @@ use std::collections::BTreeMap; +use xlsynth_vastly::CoverageCounters; +use xlsynth_vastly::Env; use xlsynth_vastly::LogicBit; use xlsynth_vastly::PipelineCycle; use xlsynth_vastly::PipelineStimulus; use xlsynth_vastly::Signedness; +use xlsynth_vastly::SourceText; use xlsynth_vastly::State; use xlsynth_vastly::Value4; use xlsynth_vastly::compile_combo_module; use xlsynth_vastly::compile_pipeline_module; use xlsynth_vastly::eval_combo; +use xlsynth_vastly::eval_combo_seeded_with_coverage; use xlsynth_vastly::plan_combo_eval; use xlsynth_vastly::run_pipeline_and_collect_outputs; @@ -166,6 +170,89 @@ endmodule assert_eq!(out["out_data"].to_bit_string_msb_first(), "10011001"); } +#[test] +fn combo_module_restores_parameter_env_after_generate_loop_shadowing() { + let dut = r#" +module m #( + parameter Shadowed = 1 +) ( + output wire hit, + output wire loop_bit +); + wire tmp; + + for (genvar Shadowed = 0; Shadowed < 1; Shadowed = Shadowed + 1) begin : g + assign tmp = 1'b0; + end + + if (Shadowed == 1) begin + assign hit = 1'b1; + end else begin + assign hit = 1'b0; + end + + assign loop_bit = tmp; +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let out = eval_combo(&m, &plan, &BTreeMap::new()).unwrap(); + + assert_eq!(out["hit"].to_bit_string_msb_first(), "1"); + assert_eq!(out["loop_bit"].to_bit_string_msb_first(), "0"); +} + +#[test] +fn combo_coverage_uses_elaborated_assign_rhs_inside_generate_loop() { + let dut = r#" +module m( + output wire [1:0] out +); + for (genvar i = 0; i < 2; i = i + 1) begin : g + assign out[i] = i; + end +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let src = SourceText::new(dut.to_string()); + let mut cov = CoverageCounters::default(); + let env = Env::new(); + + let out = + eval_combo_seeded_with_coverage(&m, &plan, &env, &src, &mut cov, &BTreeMap::new()).unwrap(); + + assert_eq!(out["out"].to_bit_string_msb_first(), "10"); +} + +#[test] +fn combo_generate_assign_preserves_unsized_signed_genvar_semantics() { + let dut = r#" +module m( + output wire [1:0] out +); + for (genvar i = 0; i < 2; i = i + 1) begin : g + assign out[i] = (i < -1); + end +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let src = SourceText::new(dut.to_string()); + let mut cov = CoverageCounters::default(); + let env = Env::new(); + + let out = eval_combo(&m, &plan, &BTreeMap::new()).unwrap(); + assert_eq!(out["out"].to_bit_string_msb_first(), "00"); + + let out = + eval_combo_seeded_with_coverage(&m, &plan, &env, &src, &mut cov, &BTreeMap::new()).unwrap(); + assert_eq!(out["out"].to_bit_string_msb_first(), "00"); +} + #[test] fn combo_module_elaborates_nested_generate_loops() { let dut = r#" diff --git a/xlsynth-vastly/tests/generated_pipeline_stage1.rs b/xlsynth-vastly/tests/generated_pipeline_stage1.rs index 0e23b2d2..3087d893 100644 --- a/xlsynth-vastly/tests/generated_pipeline_stage1.rs +++ b/xlsynth-vastly/tests/generated_pipeline_stage1.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "irvals")] use std::collections::BTreeMap; diff --git a/xlsynth-vastly/tests/irvals_inputs.rs b/xlsynth-vastly/tests/irvals_inputs.rs index 8e2d873f..decc9161 100644 --- a/xlsynth-vastly/tests/irvals_inputs.rs +++ b/xlsynth-vastly/tests/irvals_inputs.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "irvals")] use std::io::Write; diff --git a/xlsynth-vastly/tests/ternary_segments.rs b/xlsynth-vastly/tests/ternary_segments.rs index a1ed1e27..7aca7027 100644 --- a/xlsynth-vastly/tests/ternary_segments.rs +++ b/xlsynth-vastly/tests/ternary_segments.rs @@ -47,7 +47,11 @@ fn distinct_ternaries_on_same_line_are_disambiguated_by_span() { let cm = compile_pipeline_module(dut).unwrap(); let mut cov = CoverageCounters::default(); for a in &cm.combo.assigns { - cov.register_ternaries_from_spanned_expr(&a.rhs_spanned); + cov.register_ternaries_from_spanned_expr( + a.rhs_spanned + .as_ref() + .expect("coverage registration requires spanned assign expressions"), + ); } assert_eq!(cov.ternary_branches.len(), 2); let mut spans: Vec<(usize, usize)> = cov @@ -93,7 +97,11 @@ fn annotated_source_plain_shows_branch_hits() { let mut cov = CoverageCounters::default(); for a in &cm.combo.assigns { - cov.register_ternaries_from_spanned_expr(&a.rhs_spanned); + cov.register_ternaries_from_spanned_expr( + a.rhs_spanned + .as_ref() + .expect("coverage registration requires spanned assign expressions"), + ); } run_pipeline_and_collect_coverage(&cm, &stimulus, &init, &src, &mut cov).unwrap(); @@ -148,7 +156,11 @@ fn nested_ternary_shows_leaf_level_branch_hits_and_annotation() { let mut cov = CoverageCounters::default(); for a in &cm.combo.assigns { - cov.register_ternaries_from_spanned_expr(&a.rhs_spanned); + cov.register_ternaries_from_spanned_expr( + a.rhs_spanned + .as_ref() + .expect("coverage registration requires spanned assign expressions"), + ); } run_pipeline_and_collect_coverage(&cm, &stimulus, &init, &src, &mut cov).unwrap();