diff --git a/xlsynth-vastly/src/combo_compile.rs b/xlsynth-vastly/src/combo_compile.rs index 4283ab72..02f18a7d 100644 --- a/xlsynth-vastly/src/combo_compile.rs +++ b/xlsynth-vastly/src/combo_compile.rs @@ -9,12 +9,16 @@ use crate::ast::Expr; use crate::ast_spanned::SpannedExpr; use crate::ast_spanned::SpannedExprKind; use crate::module_compile::DeclInfo; +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::ComboFunctionBody; use crate::sv_ast::ComboItem; use crate::sv_ast::ComboModule; use crate::sv_ast::Decl; +use crate::sv_ast::Lhs; use crate::sv_ast::PortDir as SvPortDir; use crate::sv_ast::Span; @@ -33,12 +37,23 @@ pub struct Port { #[derive(Debug, Clone)] pub struct ComboAssign { - pub lhs: String, + pub lhs: Lhs, pub rhs: Expr, pub rhs_span: Span, pub rhs_spanned: SpannedExpr, } +impl ComboAssign { + pub fn lhs_base(&self) -> &str { + match &self.lhs { + Lhs::Ident(base) => base, + Lhs::Index { base, .. } => base, + Lhs::PackedIndex { base, .. } => base, + Lhs::Slice { base, .. } => base, + } + } +} + #[derive(Debug, Clone)] pub struct CasezPattern { pub width: u32, @@ -129,14 +144,7 @@ pub fn compile_combo_module(src: &str) -> Result { } decls.insert( normalized.denormalize_ident(&p.name), - DeclInfo { - width: p.width, - signedness: if p.signed { - Signedness::Signed - } else { - Signedness::Unsigned - }, - }, + decl_info_from_port_decl(p), ); } @@ -148,31 +156,48 @@ pub fn compile_combo_module(src: &str) -> Result { ComboItem::WireDecl(d) => { decls.insert( normalized.denormalize_ident(&d.name), - DeclInfo { - width: d.width, - signedness: if d.signed { - Signedness::Signed - } else { - Signedness::Unsigned - }, - }, + decl_info_from_decl(d), ); } - ComboItem::Assign { lhs_ident, rhs } => { + ComboItem::Assign { .. } | ComboItem::Function(_) => {} + } + } + + for it in &parsed.items { + match it { + ComboItem::WireDecl(_) => {} + ComboItem::Assign { lhs, rhs } => { let rhs_src = parse_src[rhs.start..rhs.end].trim(); let rhs_expr = denormalize_expr(parse_expr(rhs_src)?, &normalized.placeholder_to_original); + let rhs_expr = rewrite_packed_expr(rhs_expr, &decls)?; let mut rhs_spanned = parse_expr_spanned(rhs_src)?; denormalize_spanned_expr(&mut rhs_spanned, &normalized.placeholder_to_original); rhs_spanned.shift_spans(rhs.start); + rhs_spanned = rewrite_packed_spanned_expr(rhs_spanned, &decls)?; + let lhs = denormalize_lhs(lhs.clone(), &normalized.placeholder_to_original); + let lhs = rewrite_packed_lhs(lhs, &decls)?; assigns.push(ComboAssign { - lhs: normalized.denormalize_ident(lhs_ident), + lhs, rhs: rhs_expr, rhs_span: *rhs, rhs_spanned, }); } ComboItem::Function(f) => { + let mut fn_decls = decls.clone(); + for arg in &f.args { + fn_decls.insert( + normalized.denormalize_ident(&arg.name), + decl_info_from_decl(arg), + ); + } + for local in &f.locals { + fn_decls.insert( + normalized.denormalize_ident(&local.name), + decl_info_from_decl(local), + ); + } let args: Vec = f.args.iter().map(lower_decl_to_function_var).collect(); let locals: BTreeMap = f @@ -189,18 +214,26 @@ pub fn compile_combo_module(src: &str) -> Result { let body = match &f.body { ComboFunctionBody::UniqueCasez { selector, arms, .. } => { let selector_src = parse_src[selector.start..selector.end].trim(); - let selector_expr = denormalize_expr( - parse_expr(selector_src)?, - &normalized.placeholder_to_original, + let selector_expr = rewrite_packed_expr( + denormalize_expr( + parse_expr(selector_src)?, + &normalized.placeholder_to_original, + ), + &fn_decls, ); + let selector_expr = selector_expr?; 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_expr = denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, + let value_expr = rewrite_packed_expr( + denormalize_expr( + parse_expr(value_src)?, + &normalized.placeholder_to_original, + ), + &fn_decls, ); + let value_expr = value_expr?; let pat = a.pat.as_ref().map(|p| CasezPattern { width: p.width, bits_msb: p.bits_msb.clone(), @@ -217,16 +250,21 @@ pub fn compile_combo_module(src: &str) -> Result { } ComboFunctionBody::Assign { value } => { let value_src = parse_src[value.start..value.end].trim(); - let expr = denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, + let expr = rewrite_packed_expr( + denormalize_expr( + parse_expr(value_src)?, + &normalized.placeholder_to_original, + ), + &fn_decls, ); + let expr = expr?; let mut expr_spanned = parse_expr_spanned(value_src)?; denormalize_spanned_expr( &mut expr_spanned, &normalized.placeholder_to_original, ); expr_spanned.shift_spans(value.start); + expr_spanned = rewrite_packed_spanned_expr(expr_spanned, &fn_decls)?; ComboFunctionImpl::Expr { expr, expr_spanned: Some(expr_spanned), @@ -236,10 +274,14 @@ pub fn compile_combo_module(src: &str) -> Result { let mut out_assigns = Vec::with_capacity(assigns.len()); for a in assigns { let value_src = parse_src[a.value.start..a.value.end].trim(); - let expr = denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, + let expr = rewrite_packed_expr( + denormalize_expr( + parse_expr(value_src)?, + &normalized.placeholder_to_original, + ), + &fn_decls, ); + let expr = expr?; out_assigns.push(FunctionAssign { lhs: normalized.denormalize_ident(&a.lhs), expr, @@ -295,6 +337,19 @@ fn decl_info_from_decl(d: &Decl) -> DeclInfo { } else { Signedness::Unsigned }, + packed_dims: d.packed_dims.clone(), + } +} + +fn decl_info_from_port_decl(p: &crate::sv_ast::PortDecl) -> DeclInfo { + DeclInfo { + width: p.width, + signedness: if p.signed { + Signedness::Signed + } else { + Signedness::Unsigned + }, + packed_dims: p.packed_dims.clone(), } } @@ -889,6 +944,28 @@ fn denormalize_expr(expr: Expr, placeholders: &BTreeMap) -> Expr } } +fn denormalize_lhs(lhs: Lhs, placeholders: &BTreeMap) -> Lhs { + match lhs { + Lhs::Ident(name) => Lhs::Ident(placeholders.get(&name).cloned().unwrap_or(name)), + Lhs::Index { base, index } => Lhs::Index { + base: placeholders.get(&base).cloned().unwrap_or(base), + index: denormalize_expr(index, placeholders), + }, + Lhs::PackedIndex { base, indices } => Lhs::PackedIndex { + base: placeholders.get(&base).cloned().unwrap_or(base), + indices: indices + .into_iter() + .map(|e| denormalize_expr(e, placeholders)) + .collect(), + }, + Lhs::Slice { base, msb, lsb } => Lhs::Slice { + base: placeholders.get(&base).cloned().unwrap_or(base), + msb: denormalize_expr(msb, placeholders), + lsb: denormalize_expr(lsb, placeholders), + }, + } +} + fn denormalize_spanned_expr(expr: &mut SpannedExpr, placeholders: &BTreeMap) { match &mut expr.kind { SpannedExprKind::Ident(name) => { diff --git a/xlsynth-vastly/src/combo_eval.rs b/xlsynth-vastly/src/combo_eval.rs index a5b23309..ccd17aab 100644 --- a/xlsynth-vastly/src/combo_eval.rs +++ b/xlsynth-vastly/src/combo_eval.rs @@ -31,6 +31,9 @@ use crate::eval::merged_signedness; use crate::eval::operand_with_own_sign_ctx; use crate::eval::replication_count_to_u32; use crate::eval::unary_operand_expected_width; +use crate::packed::packed_index_selection; +use crate::packed::packed_index_selection_if_in_bounds; +use crate::sv_ast::Lhs; use crate::value::LogicBit; pub struct ComboEvalPlan { @@ -39,10 +42,52 @@ pub struct ComboEvalPlan { } pub fn plan_combo_eval(m: &CompiledComboModule) -> Result { - let mut lhs_to_idx: BTreeMap = BTreeMap::new(); + let mut lhs_to_idxs: BTreeMap> = BTreeMap::new(); for (i, a) in m.assigns.iter().enumerate() { - if lhs_to_idx.insert(a.lhs.clone(), i).is_some() { - return Err(Error::Parse(format!("multiple assigns to `{}`", a.lhs))); + lhs_to_idxs + .entry(a.lhs_base().to_string()) + .or_default() + .push(i); + } + let mut const_env = Env::new(); + for (name, value) in &m.consts { + const_env.insert(name.clone(), value.clone()); + } + + for (lhs, writer_indices) in &lhs_to_idxs { + if writer_indices.len() <= 1 { + continue; + } + let info = m + .decls + .get(lhs) + .ok_or_else(|| Error::Parse(format!("no decl for assign lhs `{lhs}`")))?; + let mut seen_writers_by_bit: Vec> = vec![None; info.width as usize]; + for &writer in writer_indices { + let assign = &m.assigns[writer]; + let mut deps: BTreeSet = BTreeSet::new(); + collect_idents(&assign.rhs, &mut deps); + if deps.contains(lhs) { + return Err(Error::Parse(format!( + "multiple assigns to `{lhs}` with RHS dependency on `{lhs}` are not supported" + ))); + } + let static_bits = + static_written_bits(&assign.lhs, info, &const_env).map_err(|e| match e { + Error::Parse(msg) => Error::Parse(format!( + "multiple assigns to `{lhs}` require static disjoint LHS selections: {msg}" + )), + other => other, + })?; + for bit in static_bits { + let slot = bit as usize; + if let Some(prev_writer) = seen_writers_by_bit[slot] { + return Err(Error::Parse(format!( + "overlapping assigns to `{lhs}` are not supported (assign {prev_writer} overlaps assign {writer})" + ))); + } + seen_writers_by_bit[slot] = Some(writer); + } } } @@ -53,10 +98,12 @@ pub fn plan_combo_eval(m: &CompiledComboModule) -> Result { let mut deps: BTreeSet = BTreeSet::new(); collect_idents(&a.rhs, &mut deps); for d in deps { - if let Some(&j) = lhs_to_idx.get(&d) { - // i depends on j => edge j -> i - succ[j].push(i); - indeg[i] += 1; + if let Some(writers) = lhs_to_idxs.get(&d) { + for &j in writers { + // i depends on j => edge j -> i + succ[j].push(i); + indeg[i] += 1; + } } } } @@ -161,16 +208,18 @@ pub fn eval_combo_seeded( fn eval_combo_assigns(m: &CompiledComboModule, plan: &ComboEvalPlan, env: &mut Env) -> Result<()> { for &ai in &plan.assign_order { let a = &m.assigns[ai]; + let lhs_base = a.lhs_base(); let info = m .decls - .get(&a.lhs) - .ok_or_else(|| Error::Parse(format!("no decl for assign lhs `{}`", a.lhs)))?; + .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 resolver = ComboResolver { funcs: &m.functions, globals: env, }; - let rhs_v = eval_ast_with_calls(&a.rhs, env, Some(&resolver), Some(info.width))?; - env.insert(a.lhs.clone(), coerce_to_declinfo(&rhs_v, info)); + let rhs_v = eval_ast_with_calls(&a.rhs, env, Some(&resolver), expected_width)?; + apply_lhs_to_env(&a.lhs, &rhs_v, env, info)?; } Ok(()) } @@ -199,22 +248,24 @@ pub fn eval_combo_seeded_with_coverage( } for &ai in &plan.assign_order { let a = &m.assigns[ai]; + let lhs_base = a.lhs_base(); cov.hit_span(src, a.rhs_span); let info = m .decls - .get(&a.lhs) - .ok_or_else(|| Error::Parse(format!("no decl for assign lhs `{}`", a.lhs)))?; + .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_v = eval_spanned_expr_with_funcs( &a.rhs_spanned, &env, &m.functions, - Some(info.width), + expected_width, None, cov, src, fn_meta, )?; - env.insert(a.lhs.clone(), coerce_to_declinfo(&rhs_v, info)); + apply_lhs_to_env(&a.lhs, &rhs_v, &mut env, info)?; } let mut out: BTreeMap = BTreeMap::new(); for name in m.decls.keys() { @@ -730,6 +781,7 @@ fn function_return_decl(f: &ComboFunction) -> crate::module_compile::DeclInfo { crate::module_compile::DeclInfo { width: f.ret_width, signedness: f.ret_signedness, + packed_dims: vec![f.ret_width], } } @@ -742,12 +794,191 @@ fn coerce_to_declinfo(v: &Value4, info: &crate::module_compile::DeclInfo) -> Val ) } +fn lhs_expected_write_width( + lhs: &Lhs, + info: &crate::module_compile::DeclInfo, +) -> Result> { + match lhs { + Lhs::Ident(_) => Ok(Some(info.width)), + Lhs::Index { .. } => { + let (_offset, width) = packed_index_selection(info, &[0])?; + Ok(Some(width)) + } + Lhs::PackedIndex { indices, .. } => { + let zeros = vec![0; indices.len()]; + let (_offset, width) = packed_index_selection(info, &zeros)?; + Ok(Some(width)) + } + Lhs::Slice { .. } => Ok(None), + } +} + +fn apply_lhs_to_env( + lhs: &Lhs, + rhs: &Value4, + env: &mut Env, + info: &crate::module_compile::DeclInfo, +) -> Result<()> { + match lhs { + Lhs::Ident(base) => { + env.insert(base.clone(), coerce_to_declinfo(rhs, info)); + Ok(()) + } + Lhs::Index { base, index } => { + let index_v = eval_ast_with_calls(index, env, None, None)?; + let Some(index_u) = index_v.to_u32_saturating_if_known() else { + clobber_lhs_base_to_x(base, info, env); + return Ok(()); + }; + let Some((offset, width)) = packed_index_selection_if_in_bounds(info, &[index_u])? + else { + // Out-of-bounds indexed write is a no-op. + return Ok(()); + }; + write_partial_lhs(base, info, rhs, offset, width, env); + Ok(()) + } + Lhs::PackedIndex { base, indices } => { + let mut index_values: Vec = Vec::with_capacity(indices.len()); + for index in indices { + let index_v = eval_ast_with_calls(index, env, None, None)?; + let Some(index_u) = index_v.to_u32_saturating_if_known() else { + clobber_lhs_base_to_x(base, info, env); + return Ok(()); + }; + index_values.push(index_u); + } + let Some((offset, width)) = packed_index_selection_if_in_bounds(info, &index_values)? + else { + // Out-of-bounds indexed write is a no-op. + return Ok(()); + }; + write_partial_lhs(base, info, rhs, offset, width, env); + Ok(()) + } + Lhs::Slice { base, msb, lsb } => { + let msb_v = eval_ast_with_calls(msb, env, None, None)?; + let lsb_v = eval_ast_with_calls(lsb, env, None, None)?; + let (Some(msb_u), Some(lsb_u)) = ( + msb_v.to_u32_saturating_if_known(), + lsb_v.to_u32_saturating_if_known(), + ) else { + clobber_lhs_base_to_x(base, info, env); + return Ok(()); + }; + if msb_u < lsb_u { + return Ok(()); + } + let width = msb_u - lsb_u + 1; + write_partial_lhs(base, info, rhs, lsb_u, width, env); + Ok(()) + } + } +} + +fn write_partial_lhs( + base: &str, + info: &crate::module_compile::DeclInfo, + rhs: &Value4, + offset: u32, + width: u32, + env: &mut Env, +) { + let current = env + .get(base) + .cloned() + .unwrap_or_else(|| x_value(info.width, info.signedness)); + let mut bits = current.resize(info.width).bits_lsb_first().to_vec(); + let rhs_bits = rhs.resize(width); + for i in 0..width { + let dst = offset + i; + if dst < info.width { + bits[dst as usize] = rhs_bits.bits_lsb_first()[i as usize]; + } + } + env.insert( + base.to_string(), + Value4::new(info.width, info.signedness, bits), + ); +} + +fn clobber_lhs_base_to_x(base: &str, info: &crate::module_compile::DeclInfo, env: &mut Env) { + env.insert(base.to_string(), x_value(info.width, info.signedness)); +} + +fn static_written_bits( + lhs: &Lhs, + info: &crate::module_compile::DeclInfo, + consts: &Env, +) -> Result> { + match lhs { + Lhs::Ident(_) => Ok((0..info.width).collect()), + Lhs::Index { index, .. } => { + let Some(index_u) = eval_static_u32(index, consts)? else { + return Err(Error::Parse( + "index expression is not statically known".to_string(), + )); + }; + let Some((offset, width)) = packed_index_selection_if_in_bounds(info, &[index_u])? + else { + return Ok(Vec::new()); + }; + Ok((0..width) + .map(|i| offset + i) + .filter(|bit| *bit < info.width) + .collect()) + } + Lhs::PackedIndex { indices, .. } => { + let mut values: Vec = Vec::with_capacity(indices.len()); + for index in indices { + let Some(index_u) = eval_static_u32(index, consts)? else { + return Err(Error::Parse( + "packed index expression is not statically known".to_string(), + )); + }; + values.push(index_u); + } + let Some((offset, width)) = packed_index_selection_if_in_bounds(info, &values)? else { + return Ok(Vec::new()); + }; + Ok((0..width) + .map(|i| offset + i) + .filter(|bit| *bit < info.width) + .collect()) + } + Lhs::Slice { msb, lsb, .. } => { + let Some(msb_u) = eval_static_u32(msb, consts)? else { + return Err(Error::Parse( + "slice msb expression is not statically known".to_string(), + )); + }; + let Some(lsb_u) = eval_static_u32(lsb, consts)? else { + return Err(Error::Parse( + "slice lsb expression is not statically known".to_string(), + )); + }; + if msb_u < lsb_u { + return Ok(Vec::new()); + } + Ok((lsb_u..=msb_u).filter(|bit| *bit < info.width).collect()) + } + } +} + +fn eval_static_u32(expr: &Expr, consts: &Env) -> Result> { + match eval_ast_with_calls(expr, consts, None, None) { + Ok(value) => Ok(value.to_u32_saturating_if_known()), + Err(_) => Ok(None), + } +} + fn init_function_env(f: &ComboFunction, args: &[Value4], globals: &Env) -> Env { let mut env = globals.clone(); for (arg, av) in f.args.iter().zip(args.iter()) { let info = crate::module_compile::DeclInfo { width: arg.width, signedness: arg.signedness, + packed_dims: vec![arg.width], }; env.insert(arg.name.clone(), coerce_to_declinfo(av, &info)); } @@ -772,6 +1003,7 @@ fn function_target_decl(f: &ComboFunction, lhs: &str) -> Option, } pub fn compile_module(src: &str) -> Result { @@ -39,6 +41,7 @@ pub fn compile_module(src: &str) -> Result { name, signed, width, + packed_dims, } in m.decls.clone() { let signedness = if signed { @@ -46,11 +49,20 @@ pub fn compile_module(src: &str) -> Result { } else { Signedness::Unsigned }; - decls.insert(name, DeclInfo { width, signedness }); + decls.insert( + name, + DeclInfo { + width, + signedness, + packed_dims, + }, + ); } + let body = rewrite_packed_stmt(m.always_ff.body.clone(), &decls)?; + let mut state_regs: BTreeSet = BTreeSet::new(); - collect_state_regs(&m.always_ff.body, &mut state_regs); + collect_state_regs(&body, &mut state_regs); // Require state regs declared so we can size initial X state. for r in &state_regs { @@ -68,7 +80,7 @@ pub fn compile_module(src: &str) -> Result { consts: m.params, decls, state_regs, - body: m.always_ff.body, + body, }) } @@ -119,6 +131,9 @@ fn collect_state_regs(stmt: &Stmt, out: &mut BTreeSet) { Lhs::Index { base, .. } => { out.insert(base.clone()); } + Lhs::PackedIndex { base, .. } => { + out.insert(base.clone()); + } Lhs::Slice { base, .. } => { out.insert(base.clone()); } diff --git a/xlsynth-vastly/src/module_eval.rs b/xlsynth-vastly/src/module_eval.rs index 9278ae86..a45c34cb 100644 --- a/xlsynth-vastly/src/module_eval.rs +++ b/xlsynth-vastly/src/module_eval.rs @@ -10,6 +10,7 @@ use crate::Value4; use crate::ast::Expr as VExpr; use crate::module_compile::CompiledModule; use crate::module_compile::State; +use crate::packed::packed_index_selection_if_in_bounds; use crate::sv_ast::Lhs; use crate::sv_ast::Stmt; @@ -176,11 +177,56 @@ fn apply_nba( } return Ok(()); }; - let idx = idx_u; - let bit = rhs.resize(1).bits_lsb_first()[0]; + let Some((offset, elem_width)) = packed_index_selection_if_in_bounds(info, &[idx_u])? + else { + // Out-of-bounds indexed write is a no-op. + return Ok(()); + }; + let pb = ensure_pending(pending, base, info.width); + if elem_width == 1 { + if offset < info.width { + pb.bits[offset as usize] = Some(rhs.resize(1).bits_lsb_first()[0]); + } + } else { + let rhs2 = rhs.resize(elem_width); + for i in 0..elem_width { + let dst = offset + i; + if dst < info.width { + pb.bits[dst as usize] = Some(rhs2.bits_lsb_first()[i as usize]); + } + } + } + Ok(()) + } + Lhs::PackedIndex { base, indices } => { + let info = m + .decls + .get(base) + .ok_or_else(|| Error::Parse(format!("no decl for {base}")))?; + let mut idx_vals: Vec = Vec::with_capacity(indices.len()); + for index in indices { + let idx_v = eval_expr(index, env)?; + let Some(idx_u) = idx_v.to_u32_saturating_if_known() else { + let pb = ensure_pending(pending, base, info.width); + for i in 0..(info.width as usize) { + pb.bits[i] = Some(LogicBit::X); + } + return Ok(()); + }; + idx_vals.push(idx_u); + } + let Some((offset, elem_width)) = packed_index_selection_if_in_bounds(info, &idx_vals)? + else { + // Out-of-bounds indexed write is a no-op. + return Ok(()); + }; + let rhs2 = rhs.resize(elem_width); let pb = ensure_pending(pending, base, info.width); - if idx < info.width { - pb.bits[idx as usize] = Some(bit); + for i in 0..elem_width { + let dst = offset + i; + if dst < info.width { + pb.bits[dst as usize] = Some(rhs2.bits_lsb_first()[i as usize]); + } } Ok(()) } diff --git a/xlsynth-vastly/src/packed.rs b/xlsynth-vastly/src/packed.rs new file mode 100644 index 00000000..6e44e2f0 --- /dev/null +++ b/xlsynth-vastly/src/packed.rs @@ -0,0 +1,558 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use crate::Error; +use crate::Result; +use crate::Signedness; +use crate::Value4; +use crate::ast::BinaryOp; +use crate::ast::Expr; +use crate::ast_spanned::SpannedExpr; +use crate::ast_spanned::SpannedExprKind; +use crate::module_compile::DeclInfo; +use crate::sv_ast::Lhs; +use crate::sv_ast::Span; +use crate::sv_ast::Stmt; + +fn dims_width(dims: &[u32]) -> Result { + dims.iter().try_fold(1u32, |acc, dim| { + acc.checked_mul(*dim) + .ok_or_else(|| Error::Parse("packed decl width overflow".to_string())) + }) +} + +fn stride_after(dims: &[u32], dim_index: usize) -> Result { + if dim_index + 1 >= dims.len() { + Ok(1) + } else { + dims_width(&dims[dim_index + 1..]) + } +} + +fn remaining_width_after(dims: &[u32], consumed_indices: usize) -> Result { + if consumed_indices > dims.len() { + return Err(Error::Parse( + "too many packed indices for declaration".to_string(), + )); + } + if consumed_indices == dims.len() { + Ok(1) + } else { + dims_width(&dims[consumed_indices..]) + } +} + +fn build_linear_index( + indices: &[T], + packed_dims: &[u32], + mut mul: FMul, + mut add: FAdd, +) -> Result +where + FMul: FnMut(T, u32) -> T, + FAdd: FnMut(T, T) -> T, +{ + let mut acc: Option = None; + for (dim_index, index) in indices.iter().cloned().enumerate() { + let stride = stride_after(packed_dims, dim_index)?; + let term = mul(index, stride); + acc = Some(match acc { + Some(prev) => add(prev, term), + None => term, + }); + } + acc.ok_or_else(|| Error::Parse("internal: empty packed index chain".to_string())) +} + +fn checked_packed_offset(dims: &[u32], indices: &[u32]) -> Result { + let mut offset = 0u32; + for (dim_index, index) in indices.iter().copied().enumerate() { + let dim_width = dims + .get(dim_index) + .copied() + .ok_or_else(|| Error::Parse("too many packed indices for declaration".to_string()))?; + if index >= dim_width { + return Err(Error::Parse(format!( + "packed index {index} out of bounds for dimension {dim_index} (size {dim_width})" + ))); + } + let stride = stride_after(dims, dim_index)?; + let term = index + .checked_mul(stride) + .ok_or_else(|| Error::Parse("packed index offset overflow".to_string()))?; + offset = offset + .checked_add(term) + .ok_or_else(|| Error::Parse("packed index offset overflow".to_string()))?; + } + Ok(offset) +} + +fn checked_packed_offset_if_in_bounds(dims: &[u32], indices: &[u32]) -> Result> { + let mut offset = 0u32; + for (dim_index, index) in indices.iter().copied().enumerate() { + let dim_width = dims + .get(dim_index) + .copied() + .ok_or_else(|| Error::Parse("too many packed indices for declaration".to_string()))?; + if index >= dim_width { + return Ok(None); + } + let stride = stride_after(dims, dim_index)?; + let term = index + .checked_mul(stride) + .ok_or_else(|| Error::Parse("packed index offset overflow".to_string()))?; + offset = offset + .checked_add(term) + .ok_or_else(|| Error::Parse("packed index offset overflow".to_string()))?; + } + Ok(Some(offset)) +} + +fn literal_u32(value: u32) -> Expr { + Expr::Literal( + Value4::parse_numeric_token(32, Signedness::Unsigned, &value.to_string()).unwrap(), + ) +} + +fn literal_u32_spanned(value: u32, span: Span) -> SpannedExpr { + SpannedExpr { + span, + kind: SpannedExprKind::Literal( + Value4::parse_numeric_token(32, Signedness::Unsigned, &value.to_string()).unwrap(), + ), + } +} + +fn mul_expr(lhs: Expr, factor: u32) -> Expr { + if factor == 1 { + lhs + } else { + Expr::Binary { + op: BinaryOp::Mul, + lhs: Box::new(lhs), + rhs: Box::new(literal_u32(factor)), + } + } +} + +fn mul_expr_spanned(lhs: SpannedExpr, factor: u32, span: Span) -> SpannedExpr { + if factor == 1 { + lhs + } else { + SpannedExpr { + span, + kind: SpannedExprKind::Binary { + op: BinaryOp::Mul, + lhs: Box::new(lhs), + rhs: Box::new(literal_u32_spanned(factor, span)), + }, + } + } +} + +fn add_expr(lhs: Expr, rhs: Expr) -> Expr { + Expr::Binary { + op: BinaryOp::Add, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + } +} + +fn add_expr_spanned(lhs: SpannedExpr, rhs: SpannedExpr, span: Span) -> SpannedExpr { + SpannedExpr { + span, + kind: SpannedExprKind::Binary { + op: BinaryOp::Add, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + } +} + +fn collect_index_chain(expr: &Expr) -> Option<(String, Vec)> { + match expr { + Expr::Ident(name) => Some((name.clone(), Vec::new())), + Expr::Index { expr, index } => { + let (name, mut indices) = collect_index_chain(expr)?; + indices.push((**index).clone()); + Some((name, indices)) + } + _ => None, + } +} + +fn collect_spanned_index_chain(expr: &SpannedExpr) -> Option<(SpannedExpr, Vec)> { + match &expr.kind { + SpannedExprKind::Ident(_) => Some((expr.clone(), Vec::new())), + SpannedExprKind::Index { expr: inner, index } => { + let (base, mut indices) = collect_spanned_index_chain(inner)?; + indices.push((**index).clone()); + Some((base, indices)) + } + _ => None, + } +} + +fn lower_expr_index_chain( + base_name: &str, + indices: Vec, + decls: &BTreeMap, +) -> Result> { + let Some(info) = decls.get(base_name) else { + return Ok(None); + }; + if indices.is_empty() { + return Ok(None); + } + if indices.len() > info.packed_dims.len() { + return Err(Error::Parse(format!( + "too many packed indices for `{base_name}`" + ))); + } + let linear_index = build_linear_index(&indices, &info.packed_dims, mul_expr, add_expr)?; + let remaining_width = remaining_width_after(&info.packed_dims, indices.len())?; + let base_expr = Expr::Ident(base_name.to_string()); + if remaining_width == 1 { + Ok(Some(Expr::Index { + expr: Box::new(base_expr), + index: Box::new(linear_index), + })) + } else { + Ok(Some(Expr::IndexedSlice { + expr: Box::new(base_expr), + base: Box::new(linear_index), + width: Box::new(literal_u32(remaining_width)), + upward: true, + })) + } +} + +fn lower_spanned_index_chain( + expr_span: Span, + base: SpannedExpr, + indices: Vec, + decls: &BTreeMap, +) -> Result> { + let SpannedExprKind::Ident(base_name) = &base.kind else { + return Ok(None); + }; + let Some(info) = decls.get(base_name) else { + return Ok(None); + }; + if indices.is_empty() { + return Ok(None); + } + if indices.len() > info.packed_dims.len() { + return Err(Error::Parse(format!( + "too many packed indices for `{base_name}`" + ))); + } + let linear_index = build_linear_index( + &indices, + &info.packed_dims, + |index, stride| mul_expr_spanned(index, stride, expr_span), + |lhs, rhs| add_expr_spanned(lhs, rhs, expr_span), + )?; + let remaining_width = remaining_width_after(&info.packed_dims, indices.len())?; + if remaining_width == 1 { + Ok(Some(SpannedExpr { + span: expr_span, + kind: SpannedExprKind::Index { + expr: Box::new(base), + index: Box::new(linear_index), + }, + })) + } else { + Ok(Some(SpannedExpr { + span: expr_span, + kind: SpannedExprKind::IndexedSlice { + expr: Box::new(base), + base: Box::new(linear_index), + width: Box::new(literal_u32_spanned(remaining_width, expr_span)), + upward: true, + }, + })) + } +} + +pub fn rewrite_packed_expr(expr: Expr, decls: &BTreeMap) -> Result { + if let Some((base_name, indices)) = collect_index_chain(&expr) { + if !indices.is_empty() { + let mut rewritten_indices = Vec::with_capacity(indices.len()); + for index in indices { + rewritten_indices.push(rewrite_packed_expr(index, decls)?); + } + if let Some(lowered) = lower_expr_index_chain(&base_name, rewritten_indices, decls)? { + return Ok(lowered); + } + } + } + match expr { + Expr::Ident(_) | Expr::Literal(_) | Expr::UnsizedNumber(_) | Expr::UnbasedUnsized(_) => { + Ok(expr) + } + Expr::Call { name, args } => { + let mut out = Vec::with_capacity(args.len()); + for arg in args { + out.push(rewrite_packed_expr(arg, decls)?); + } + Ok(Expr::Call { name, args: out }) + } + Expr::Concat(parts) => { + let mut out = Vec::with_capacity(parts.len()); + for part in parts { + out.push(rewrite_packed_expr(part, decls)?); + } + Ok(Expr::Concat(out)) + } + Expr::Replicate { count, expr } => Ok(Expr::Replicate { + count: Box::new(rewrite_packed_expr(*count, decls)?), + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + }), + Expr::Cast { width, expr } => Ok(Expr::Cast { + width: Box::new(rewrite_packed_expr(*width, decls)?), + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + }), + Expr::Index { expr, index } => Ok(Expr::Index { + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + index: Box::new(rewrite_packed_expr(*index, decls)?), + }), + Expr::Slice { expr, msb, lsb } => Ok(Expr::Slice { + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + msb: Box::new(rewrite_packed_expr(*msb, decls)?), + lsb: Box::new(rewrite_packed_expr(*lsb, decls)?), + }), + Expr::IndexedSlice { + expr, + base, + width, + upward, + } => Ok(Expr::IndexedSlice { + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + base: Box::new(rewrite_packed_expr(*base, decls)?), + width: Box::new(rewrite_packed_expr(*width, decls)?), + upward, + }), + Expr::Unary { op, expr } => Ok(Expr::Unary { + op, + expr: Box::new(rewrite_packed_expr(*expr, decls)?), + }), + Expr::Binary { op, lhs, rhs } => Ok(Expr::Binary { + op, + lhs: Box::new(rewrite_packed_expr(*lhs, decls)?), + rhs: Box::new(rewrite_packed_expr(*rhs, decls)?), + }), + Expr::Ternary { cond, t, f } => Ok(Expr::Ternary { + cond: Box::new(rewrite_packed_expr(*cond, decls)?), + t: Box::new(rewrite_packed_expr(*t, decls)?), + f: Box::new(rewrite_packed_expr(*f, decls)?), + }), + } +} + +pub fn rewrite_packed_spanned_expr( + expr: SpannedExpr, + decls: &BTreeMap, +) -> Result { + if let Some((base, indices)) = collect_spanned_index_chain(&expr) { + if !indices.is_empty() { + let mut rewritten_indices = Vec::with_capacity(indices.len()); + for index in indices { + rewritten_indices.push(rewrite_packed_spanned_expr(index, decls)?); + } + if let Some(lowered) = + lower_spanned_index_chain(expr.span, base, rewritten_indices, decls)? + { + return Ok(lowered); + } + } + } + match expr.kind { + SpannedExprKind::Ident(_) + | SpannedExprKind::Literal(_) + | SpannedExprKind::UnsizedNumber(_) + | SpannedExprKind::UnbasedUnsized(_) => Ok(expr), + SpannedExprKind::Call { name, args } => { + let mut out = Vec::with_capacity(args.len()); + for arg in args { + out.push(rewrite_packed_spanned_expr(arg, decls)?); + } + Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Call { name, args: out }, + }) + } + SpannedExprKind::Concat(parts) => { + let mut out = Vec::with_capacity(parts.len()); + for part in parts { + out.push(rewrite_packed_spanned_expr(part, decls)?); + } + Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Concat(out), + }) + } + SpannedExprKind::Replicate { count, expr: inner } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Replicate { + count: Box::new(rewrite_packed_spanned_expr(*count, decls)?), + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + }, + }), + SpannedExprKind::Cast { width, expr: inner } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Cast { + width: Box::new(rewrite_packed_spanned_expr(*width, decls)?), + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + }, + }), + SpannedExprKind::Index { expr: inner, index } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Index { + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + index: Box::new(rewrite_packed_spanned_expr(*index, decls)?), + }, + }), + SpannedExprKind::Slice { + expr: inner, + msb, + lsb, + } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Slice { + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + msb: Box::new(rewrite_packed_spanned_expr(*msb, decls)?), + lsb: Box::new(rewrite_packed_spanned_expr(*lsb, decls)?), + }, + }), + SpannedExprKind::IndexedSlice { + expr: inner, + base, + width, + upward, + } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::IndexedSlice { + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + base: Box::new(rewrite_packed_spanned_expr(*base, decls)?), + width: Box::new(rewrite_packed_spanned_expr(*width, decls)?), + upward, + }, + }), + SpannedExprKind::Unary { op, expr: inner } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Unary { + op, + expr: Box::new(rewrite_packed_spanned_expr(*inner, decls)?), + }, + }), + SpannedExprKind::Binary { op, lhs, rhs } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Binary { + op, + lhs: Box::new(rewrite_packed_spanned_expr(*lhs, decls)?), + rhs: Box::new(rewrite_packed_spanned_expr(*rhs, decls)?), + }, + }), + SpannedExprKind::Ternary { cond, t, f } => Ok(SpannedExpr { + span: expr.span, + kind: SpannedExprKind::Ternary { + cond: Box::new(rewrite_packed_spanned_expr(*cond, decls)?), + t: Box::new(rewrite_packed_spanned_expr(*t, decls)?), + f: Box::new(rewrite_packed_spanned_expr(*f, decls)?), + }, + }), + } +} + +pub fn rewrite_packed_lhs(lhs: Lhs, decls: &BTreeMap) -> Result { + match lhs { + Lhs::Ident(_) => Ok(lhs), + Lhs::Index { base, index } => Ok(Lhs::Index { + base, + index: rewrite_packed_expr(index, decls)?, + }), + Lhs::PackedIndex { base, indices } => { + let mut out = Vec::with_capacity(indices.len()); + for index in indices { + out.push(rewrite_packed_expr(index, decls)?); + } + Ok(Lhs::PackedIndex { base, indices: out }) + } + Lhs::Slice { base, msb, lsb } => Ok(Lhs::Slice { + base, + msb: rewrite_packed_expr(msb, decls)?, + lsb: rewrite_packed_expr(lsb, decls)?, + }), + } +} + +pub fn rewrite_packed_stmt(stmt: Stmt, decls: &BTreeMap) -> Result { + match stmt { + Stmt::Begin(stmts) => { + let mut out = Vec::with_capacity(stmts.len()); + for stmt in stmts { + out.push(rewrite_packed_stmt(stmt, decls)?); + } + Ok(Stmt::Begin(out)) + } + Stmt::If { + cond, + then_branch, + else_branch, + } => Ok(Stmt::If { + cond: rewrite_packed_expr(cond, decls)?, + then_branch: Box::new(rewrite_packed_stmt(*then_branch, decls)?), + else_branch: match else_branch { + Some(stmt) => Some(Box::new(rewrite_packed_stmt(*stmt, decls)?)), + None => None, + }, + }), + Stmt::NbaAssign { lhs, rhs } => Ok(Stmt::NbaAssign { + lhs: rewrite_packed_lhs(lhs, decls)?, + rhs: rewrite_packed_expr(rhs, decls)?, + }), + Stmt::Display { fmt, args } => { + let mut out = Vec::with_capacity(args.len()); + for arg in args { + out.push(rewrite_packed_expr(arg, decls)?); + } + Ok(Stmt::Display { fmt, args: out }) + } + Stmt::Empty => Ok(Stmt::Empty), + } +} + +pub fn packed_index_selection(info: &DeclInfo, indices: &[u32]) -> Result<(u32, u32)> { + if indices.is_empty() { + return Ok((0, info.width)); + } + if indices.len() > info.packed_dims.len() { + return Err(Error::Parse( + "too many packed indices for declaration".to_string(), + )); + } + let offset = checked_packed_offset(&info.packed_dims, indices)?; + let width = remaining_width_after(&info.packed_dims, indices.len())?; + Ok((offset, width)) +} + +pub fn packed_index_selection_if_in_bounds( + info: &DeclInfo, + indices: &[u32], +) -> Result> { + if indices.is_empty() { + return Ok(Some((0, info.width))); + } + if indices.len() > info.packed_dims.len() { + return Err(Error::Parse( + "too many packed indices for declaration".to_string(), + )); + } + let Some(offset) = checked_packed_offset_if_in_bounds(&info.packed_dims, indices)? else { + return Ok(None); + }; + let width = remaining_width_after(&info.packed_dims, indices.len())?; + Ok(Some((offset, width))) +} diff --git a/xlsynth-vastly/src/pipeline_compile.rs b/xlsynth-vastly/src/pipeline_compile.rs index adb9d1eb..d522c50e 100644 --- a/xlsynth-vastly/src/pipeline_compile.rs +++ b/xlsynth-vastly/src/pipeline_compile.rs @@ -20,6 +20,9 @@ use crate::combo_compile::Port; use crate::combo_compile::PortDir; use crate::module_compile::CompiledModule; use crate::module_compile::DeclInfo; +use crate::packed::rewrite_packed_expr; +use crate::packed::rewrite_packed_spanned_expr; +use crate::packed::rewrite_packed_stmt; use crate::sim_observer::SimObserver; use crate::sv_ast::Lhs; use crate::sv_ast::PipelineItem; @@ -106,22 +109,12 @@ pub fn compile_pipeline_module_with_defines( PortDir::Input => input_ports_all.push(port), PortDir::Output => output_ports.push(port), } - decls.insert( - p.name.clone(), - DeclInfo { - width: p.width, - signedness: if p.signed { - Signedness::Signed - } else { - Signedness::Unsigned - }, - }, - ); + decls.insert(p.name.clone(), decl_info_from_port_decl(p)); } let mut assigns: Vec = Vec::new(); let mut functions: BTreeMap = BTreeMap::new(); - let mut always_ffs: Vec = Vec::new(); + let mut always_ffs: Vec<(crate::sv_ast::AlwaysFf, Span)> = Vec::new(); let mut fn_meta: BTreeMap = BTreeMap::new(); let mut observers: Vec = Vec::new(); let mut observer_spans: Vec = Vec::new(); @@ -132,25 +125,25 @@ pub fn compile_pipeline_module_with_defines( if decls.contains_key(&d.name) { return Err(Error::Parse(format!("duplicate decl `{}`", d.name))); } - decls.insert( - d.name.clone(), - DeclInfo { - width: d.width, - signedness: if d.signed { - Signedness::Signed - } else { - Signedness::Unsigned - }, - }, - ); + decls.insert(d.name.clone(), decl_info_from_decl(d)); } + PipelineItem::Assign { .. } + | PipelineItem::Function { .. } + | PipelineItem::AlwaysFf { .. } => {} + } + } + + for it in &parsed.items { + match it { + PipelineItem::Decl { .. } => {} PipelineItem::Assign { lhs_ident, rhs, .. } => { let rhs_src = src[rhs.start..rhs.end].trim(); - let rhs_expr = crate::parser::parse_expr(rhs_src)?; + let rhs_expr = rewrite_packed_expr(crate::parser::parse_expr(rhs_src)?, &decls)?; let mut rhs_spanned = crate::parser_spanned::parse_expr_spanned(rhs_src)?; rhs_spanned.shift_spans(rhs.start); + rhs_spanned = rewrite_packed_spanned_expr(rhs_spanned, &decls)?; assigns.push(ComboAssign { - lhs: lhs_ident.clone(), + lhs: Lhs::Ident(lhs_ident.clone()), rhs: rhs_expr, rhs_span: *rhs, rhs_spanned, @@ -172,6 +165,13 @@ pub fn compile_pipeline_module_with_defines( f.name ))); } + let mut fn_decls = decls.clone(); + for arg in &f.args { + fn_decls.insert(arg.name.clone(), decl_info_from_decl(arg)); + } + for local in &f.locals { + fn_decls.insert(local.name.clone(), decl_info_from_decl(local)); + } let args: Vec = f .args .iter() @@ -188,30 +188,24 @@ pub fn compile_pipeline_module_with_defines( let locals: BTreeMap = f .locals .iter() - .map(|d| { - ( - d.name.clone(), - DeclInfo { - width: d.width, - signedness: if d.signed { - Signedness::Signed - } else { - Signedness::Unsigned - }, - }, - ) - }) + .map(|d| (d.name.clone(), decl_info_from_decl(d))) .collect(); let body = match &f.body { crate::sv_ast::ComboFunctionBody::UniqueCasez { selector, arms, .. } => { let selector_src = src[selector.start..selector.end].trim(); - let selector_expr = crate::parser::parse_expr(selector_src)?; + let selector_expr = rewrite_packed_expr( + crate::parser::parse_expr(selector_src)?, + &fn_decls, + )?; let mut out_arms: Vec = Vec::new(); for a in arms { let value_src = src[a.value.start..a.value.end].trim(); - let value_expr = crate::parser::parse_expr(value_src)?; + let value_expr = rewrite_packed_expr( + crate::parser::parse_expr(value_src)?, + &fn_decls, + )?; let pat = a.pat.as_ref().map(|p| CasezPattern { width: p.width, bits_msb: p.bits_msb.clone(), @@ -228,10 +222,12 @@ pub fn compile_pipeline_module_with_defines( } crate::sv_ast::ComboFunctionBody::Assign { value } => { let value_src = src[value.start..value.end].trim(); - let expr = crate::parser::parse_expr(value_src)?; + let expr = + rewrite_packed_expr(crate::parser::parse_expr(value_src)?, &fn_decls)?; let mut expr_spanned = crate::parser_spanned::parse_expr_spanned(value_src)?; expr_spanned.shift_spans(value.start); + expr_spanned = rewrite_packed_spanned_expr(expr_spanned, &fn_decls)?; ComboFunctionImpl::Expr { expr, expr_spanned: Some(expr_spanned), @@ -242,7 +238,10 @@ pub fn compile_pipeline_module_with_defines( Vec::with_capacity(assigns.len()); for a in assigns { let value_src = src[a.value.start..a.value.end].trim(); - let expr = crate::parser::parse_expr(value_src)?; + let expr = rewrite_packed_expr( + crate::parser::parse_expr(value_src)?, + &fn_decls, + )?; out_assigns.push(FunctionAssign { lhs: a.lhs.clone(), expr, @@ -311,15 +310,24 @@ pub fn compile_pipeline_module_with_defines( }, ); } - PipelineItem::AlwaysFf { always_ff: af, .. } => { - always_ffs.push(af.clone()); + PipelineItem::AlwaysFf { + always_ff: af, + span, + } => { + always_ffs.push(( + crate::sv_ast::AlwaysFf { + clk_name: af.clk_name.clone(), + body: rewrite_packed_stmt(af.body.clone(), &decls)?, + }, + *span, + )); } } } let clk_name = always_ffs .first() - .map(|af| af.clk_name.clone()) + .map(|(af, _)| af.clk_name.clone()) .or_else(|| { let has_clk = input_ports_all.iter().any(|p| p.name == "clk"); if has_clk { @@ -334,20 +342,10 @@ pub fn compile_pipeline_module_with_defines( ) })?; - // Partition always_ff blocks into stateful seq blocks and observer blocks. - // Also remember per-item spans so we can attribute coverage. - let mut always_ff_items: Vec<(crate::sv_ast::AlwaysFf, Span)> = Vec::new(); - for it in &parsed.items { - if let PipelineItem::AlwaysFf { - always_ff: af, - span, - } = it - { - always_ff_items.push((af.clone(), *span)); - } - } - - for (af, af_span) in &always_ff_items { + // Partition always_ff blocks into zero or more stateful seq blocks and observer + // blocks. + let mut stateful: Vec<(crate::sv_ast::AlwaysFf, Span)> = Vec::new(); + for (af, af_span) in always_ffs { if af.clk_name != clk_name { return Err(Error::Parse(format!( "always_ff clock mismatch: saw `{}` but expected `{}`", @@ -362,9 +360,10 @@ pub fn compile_pipeline_module_with_defines( "mixed nba assigns and $display in one always_ff is not supported".to_string(), )); } + stateful.push((af, af_span)); } else if has_disp { observers.extend(crate::sim_observer::extract_observers(&clk_name, &af.body)?); - observer_spans.push(*af_span); + observer_spans.push(af_span); } else { // Empty / unsupported: ignore (v1). } @@ -386,14 +385,9 @@ pub fn compile_pipeline_module_with_defines( }; let mut seen_state_regs: BTreeSet = BTreeSet::new(); - let mut seqs: Vec = Vec::new(); + let mut seqs: Vec = Vec::with_capacity(stateful.len()); let mut seq_spans: Vec = Vec::new(); - for (af, af_span) in always_ff_items { - let has_nba = stmt_contains_nba(&af.body); - let has_disp = stmt_contains_display(&af.body); - if !has_nba || has_disp { - continue; - } + for (af, af_span) in stateful { let mut state_regs: BTreeSet = BTreeSet::new(); collect_state_regs(&af.body, &mut state_regs); for r in &state_regs { @@ -473,6 +467,30 @@ fn stmt_contains_display(stmt: &Stmt) -> bool { } } +fn decl_info_from_decl(d: &crate::sv_ast::Decl) -> DeclInfo { + DeclInfo { + width: d.width, + signedness: if d.signed { + Signedness::Signed + } else { + Signedness::Unsigned + }, + packed_dims: d.packed_dims.clone(), + } +} + +fn decl_info_from_port_decl(p: &crate::sv_ast::PortDecl) -> DeclInfo { + DeclInfo { + width: p.width, + signedness: if p.signed { + Signedness::Signed + } else { + Signedness::Unsigned + }, + packed_dims: p.packed_dims.clone(), + } +} + fn collect_state_regs(stmt: &Stmt, out: &mut BTreeSet) { match stmt { Stmt::Begin(stmts) => { @@ -497,6 +515,9 @@ fn collect_state_regs(stmt: &Stmt, out: &mut BTreeSet) { Lhs::Index { base, .. } => { out.insert(base.clone()); } + Lhs::PackedIndex { base, .. } => { + out.insert(base.clone()); + } Lhs::Slice { base, .. } => { out.insert(base.clone()); } diff --git a/xlsynth-vastly/src/pipeline_harness.rs b/xlsynth-vastly/src/pipeline_harness.rs index 3e0a2b2f..65ab42be 100644 --- a/xlsynth-vastly/src/pipeline_harness.rs +++ b/xlsynth-vastly/src/pipeline_harness.rs @@ -406,13 +406,13 @@ fn literal_assigned_names(m: &crate::pipeline_compile::CompiledPipelineModule) - for a in &m.combo.assigns { match &a.rhs_spanned.kind { crate::ast_spanned::SpannedExprKind::Literal(_) => { - s.insert(a.lhs.clone()); + s.insert(a.lhs_base().to_string()); } crate::ast_spanned::SpannedExprKind::UnsizedNumber(_) => { - s.insert(a.lhs.clone()); + s.insert(a.lhs_base().to_string()); } crate::ast_spanned::SpannedExprKind::UnbasedUnsized(_) => { - s.insert(a.lhs.clone()); + s.insert(a.lhs_base().to_string()); } _ => {} } diff --git a/xlsynth-vastly/src/sv_ast.rs b/xlsynth-vastly/src/sv_ast.rs index 75acb05f..07e75ce6 100644 --- a/xlsynth-vastly/src/sv_ast.rs +++ b/xlsynth-vastly/src/sv_ast.rs @@ -28,6 +28,7 @@ pub struct PortDecl { pub ty: PortTy, pub signed: bool, pub width: u32, + pub packed_dims: Vec, pub name: String, } @@ -42,7 +43,7 @@ pub struct ComboModule { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ComboItem { WireDecl(Decl), - Assign { lhs_ident: String, rhs: Span }, + Assign { lhs: Lhs, rhs: Span }, Function(ComboFunction), } @@ -107,6 +108,7 @@ pub struct Decl { pub name: String, pub signed: bool, pub width: u32, + pub packed_dims: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -175,6 +177,10 @@ pub enum Lhs { base: String, index: VExpr, }, + PackedIndex { + base: String, + indices: Vec, + }, Slice { base: String, msb: VExpr, diff --git a/xlsynth-vastly/src/sv_parser.rs b/xlsynth-vastly/src/sv_parser.rs index 39435888..b94de573 100644 --- a/xlsynth-vastly/src/sv_parser.rs +++ b/xlsynth-vastly/src/sv_parser.rs @@ -613,22 +613,8 @@ impl<'a> Parser<'a> { false }; - let mut width: u32 = 1; - if *self.cur() == TokKind::LBracket { - // [msb:lsb] - self.bump(); - let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; - self.expect(TokKind::Colon)?; - let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; - self.expect(TokKind::RBracket)?; - - let msb = self.eval_const_u32(&msb_expr)?; - let lsb = self.eval_const_u32(&lsb_expr)?; - if msb < lsb { - return Err(Error::Parse("decl range msb { @@ -642,6 +628,7 @@ impl<'a> Parser<'a> { name, signed, width, + packed_dims, }); match self.cur() { @@ -704,20 +691,8 @@ impl<'a> Parser<'a> { false }; - let mut width: u32 = 1; - if *self.cur() == TokKind::LBracket { - self.bump(); - let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; - self.expect(TokKind::Colon)?; - let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; - self.expect(TokKind::RBracket)?; - let msb = self.eval_const_u32(&msb_expr)?; - let lsb = self.eval_const_u32(&lsb_expr)?; - if msb < lsb { - return Err(Error::Parse("decl range msb { @@ -732,6 +707,7 @@ impl<'a> Parser<'a> { ty, signed, width, + packed_dims, name, }); @@ -747,6 +723,27 @@ impl<'a> Parser<'a> { Ok(ports) } + fn parse_packed_dims(&mut self) -> Result> { + let mut dims: Vec = Vec::new(); + while *self.cur() == TokKind::LBracket { + self.bump(); + let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; + self.expect(TokKind::Colon)?; + let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; + self.expect(TokKind::RBracket)?; + let msb = self.eval_const_u32(&msb_expr)?; + let lsb = self.eval_const_u32(&lsb_expr)?; + if msb < lsb { + return Err(Error::Parse("decl range msb Result { let (d, _span) = self.parse_wire_decl_with_span()?; Ok(d) @@ -763,20 +760,8 @@ impl<'a> Parser<'a> { false }; - let mut width: u32 = 1; - if *self.cur() == TokKind::LBracket { - self.bump(); - let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; - self.expect(TokKind::Colon)?; - let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; - self.expect(TokKind::RBracket)?; - let msb = self.eval_const_u32(&msb_expr)?; - let lsb = self.eval_const_u32(&lsb_expr)?; - if msb < lsb { - return Err(Error::Parse("decl range msb { @@ -792,6 +777,7 @@ impl<'a> Parser<'a> { name, signed, width, + packed_dims, }, Span { start, end }, )) @@ -806,21 +792,11 @@ impl<'a> Parser<'a> { fn parse_assign_item(&mut self) -> Result { self.expect(TokKind::KwAssign)?; - let lhs_ident = match self.toks[self.idx].kind.clone() { - TokKind::Ident(s) => { - self.bump(); - s - } - _ => { - return Err(Error::Parse( - "expected identifier on assign LHS".to_string(), - )); - } - }; + let lhs = self.parse_lhs()?; self.expect(TokKind::Eq)?; let rhs = self.parse_span_until_semi()?; self.expect(TokKind::Semi)?; - Ok(ComboItem::Assign { lhs_ident, rhs }) + Ok(ComboItem::Assign { lhs, rhs }) } fn parse_combo_function(&mut self) -> Result { @@ -1004,20 +980,8 @@ impl<'a> Parser<'a> { false }; - let mut width: u32 = 1; - if *self.cur() == TokKind::LBracket { - self.bump(); - let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; - self.expect(TokKind::Colon)?; - let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; - self.expect(TokKind::RBracket)?; - let msb = self.eval_const_u32(&msb_expr)?; - let lsb = self.eval_const_u32(&lsb_expr)?; - if msb < lsb { - return Err(Error::Parse("decl range msb { @@ -1033,6 +997,7 @@ impl<'a> Parser<'a> { name, signed, width, + packed_dims, }) } @@ -1162,22 +1127,8 @@ impl<'a> Parser<'a> { false }; - let mut width: u32 = 1; - if *self.cur() == TokKind::LBracket { - // [msb:lsb] - self.bump(); - let msb_expr = self.parse_expr_until(&[TokKind::Colon])?; - self.expect(TokKind::Colon)?; - let lsb_expr = self.parse_expr_until(&[TokKind::RBracket])?; - self.expect(TokKind::RBracket)?; - - let msb = self.eval_const_u32(&msb_expr)?; - let lsb = self.eval_const_u32(&lsb_expr)?; - if msb < lsb { - return Err(Error::Parse("decl range msb { @@ -1191,6 +1142,7 @@ impl<'a> Parser<'a> { name, signed, width, + packed_dims, }) } @@ -1313,20 +1265,43 @@ impl<'a> Parser<'a> { if *self.cur() != TokKind::LBracket { return Ok(Lhs::Ident(base)); } - self.expect(TokKind::LBracket)?; - let first = self.parse_expr_until(&[TokKind::Colon, TokKind::RBracket])?; - if *self.cur() == TokKind::Colon { - self.bump(); - let lsb = self.parse_expr_until(&[TokKind::RBracket])?; + let mut indices: Vec = Vec::new(); + loop { + self.expect(TokKind::LBracket)?; + let first = self.parse_expr_until(&[TokKind::Colon, TokKind::RBracket])?; + if *self.cur() == TokKind::Colon { + if !indices.is_empty() { + return Err(Error::Parse( + "mixed packed index and slice lhs not supported".to_string(), + )); + } + self.bump(); + let lsb = self.parse_expr_until(&[TokKind::RBracket])?; + self.expect(TokKind::RBracket)?; + if *self.cur() == TokKind::LBracket { + return Err(Error::Parse( + "multiple bracket groups after slice lhs not supported".to_string(), + )); + } + return Ok(Lhs::Slice { + base, + msb: first, + lsb, + }); + } self.expect(TokKind::RBracket)?; - Ok(Lhs::Slice { + indices.push(first); + if *self.cur() != TokKind::LBracket { + break; + } + } + if indices.len() == 1 { + Ok(Lhs::Index { base, - msb: first, - lsb, + index: indices.pop().expect("checked len"), }) } else { - self.expect(TokKind::RBracket)?; - Ok(Lhs::Index { base, index: first }) + Ok(Lhs::PackedIndex { base, indices }) } } @@ -1430,6 +1405,13 @@ fn parse_casez_pattern(s: &str) -> Result { }) } +fn packed_dims_width(dims: &[u32]) -> Result { + dims.iter().try_fold(1u32, |acc, dim| { + acc.checked_mul(*dim) + .ok_or_else(|| Error::Parse("packed decl width overflow".to_string())) + }) +} + fn parse_display_call(s: &str) -> Result<(String, Vec)> { // Very small `$display` parser for forms like: // $display("foo %d", expr, expr2) diff --git a/xlsynth-vastly/tests/function_coverage.rs b/xlsynth-vastly/tests/function_coverage.rs index 62867194..b29ac1e5 100644 --- a/xlsynth-vastly/tests/function_coverage.rs +++ b/xlsynth-vastly/tests/function_coverage.rs @@ -316,6 +316,62 @@ fn function_expr_ternary_equality_recontexts_unbased_unsized_rhs() { assert_eq!(counts.cond_unknown, 0); } +#[test] +fn function_expr_packed_index_matches_plain_eval_in_coverage_mode() { + let dut = concat!( + "module m(input logic clk, input logic [1:0][3:0] a, input logic idx, output logic [3:0] y);\n", + " function automatic logic [3:0] pick(input logic i);\n", + " begin\n", + " pick = a[i];\n", + " end\n", + " endfunction\n", + " assign y = pick(idx);\n", + "endmodule\n", + ); + let cm = compile_pipeline_module(dut).unwrap(); + let src = SourceText::new(dut.to_string()); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("idx".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("idx".to_string(), vbits(1, Signedness::Unsigned, "1")), + ] + .into_iter() + .collect(), + }, + ], + }; + + let (coverage_steps, _cov) = + run_combinational_pipeline_with_output_equivalence(&cm, &stimulus, &src); + + let ys: Vec = coverage_steps + .into_iter() + .map(|snapshot| snapshot.get("y").unwrap().clone()) + .collect(); + assert_eq!( + ys, + vec![ + "0011".to_string(), + "0011".to_string(), + "0011".to_string(), + "1010".to_string(), + "1010".to_string(), + "1010".to_string(), + ] + ); +} + #[test] fn function_expr_ternary_unbased_unsized_rhs_matrix_tracks_outputs_and_counts() { let lhs_decls = ["logic [3:0]", "logic signed [3:0]"]; diff --git a/xlsynth-vastly/tests/packed_arrays.rs b/xlsynth-vastly/tests/packed_arrays.rs new file mode 100644 index 00000000..1586a564 --- /dev/null +++ b/xlsynth-vastly/tests/packed_arrays.rs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use xlsynth_vastly::LogicBit; +use xlsynth_vastly::PipelineCycle; +use xlsynth_vastly::PipelineStimulus; +use xlsynth_vastly::Signedness; +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::plan_combo_eval; +use xlsynth_vastly::run_pipeline_and_collect_outputs; + +fn vbits(width: u32, signedness: Signedness, msb: &str) -> Value4 { + assert_eq!(msb.len(), width as usize); + let mut bits = Vec::with_capacity(width as usize); + for c in msb.chars().rev() { + bits.push(match c { + '0' => LogicBit::Zero, + '1' => LogicBit::One, + 'x' | 'X' => LogicBit::X, + 'z' | 'Z' => LogicBit::Z, + _ => panic!("bad bit char {c}"), + }); + } + Value4::new(width, signedness, bits) +} + +#[test] +fn combo_module_supports_packed_array_ports_and_decls() { + let dut = r#" +module m( + input logic [1:0][3:0] a, + input logic sel, + output logic [3:0] y +); + logic [1:0][3:0] tmp; + assign tmp = a; + assign y = tmp[sel]; +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + assert_eq!(m.decls.get("a").unwrap().width, 8); + assert_eq!(m.decls.get("tmp").unwrap().width, 8); + + let plan = plan_combo_eval(&m).unwrap(); + let inputs0: BTreeMap = [ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect(); + let values0 = eval_combo(&m, &plan, &inputs0).unwrap(); + assert_eq!(values0.get("y").unwrap().to_bit_string_msb_first(), "0011"); + + let inputs1: BTreeMap = [ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "1")), + ] + .into_iter() + .collect(); + let values1 = eval_combo(&m, &plan, &inputs1).unwrap(); + assert_eq!(values1.get("y").unwrap().to_bit_string_msb_first(), "1010"); +} + +#[test] +fn pipeline_module_updates_packed_array_elements() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0][1:0][3:0] in_data, + output logic [1:0][1:0][3:0] out_data, + output logic [3:0] tap +); + logic [1:0][1:0][3:0] q; + + always_ff @(posedge clk) begin + if (rst) begin + q[0][0] <= 4'h1; + q[0][1] <= 4'h2; + q[1][0] <= 4'h3; + q[1][1] <= 4'h4; + end else begin + q[0][0] <= in_data[1][1]; + q[0][1] <= in_data[1][0]; + q[1][0] <= in_data[0][1]; + q[1][1] <= in_data[0][0]; + end + end + + assign out_data = q; + assign tap = q[1][0]; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + assert_eq!(m.combo.decls.get("in_data").unwrap().width, 16); + assert_eq!(m.combo.decls.get("q").unwrap().width, 16); + + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ( + "in_data".to_string(), + vbits(16, Signedness::Unsigned, "0000000000000000"), + ), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ( + "in_data".to_string(), + vbits(16, Signedness::Unsigned, "1111111011011100"), + ), + ] + .into_iter() + .collect(), + }, + ], + }; + + let init: State = BTreeMap::new(); + let outputs = run_pipeline_and_collect_outputs(&m, &stimulus, &init).unwrap(); + + assert_eq!( + outputs[0] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "0100001100100001" + ); + assert_eq!( + outputs[0].get("tap").unwrap().to_bit_string_msb_first(), + "0011" + ); + + assert_eq!( + outputs[1] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "1100110111101111" + ); + assert_eq!( + outputs[1].get("tap").unwrap().to_bit_string_msb_first(), + "1101" + ); +} diff --git a/xlsynth-vastly/tests/parse_combo_module.rs b/xlsynth-vastly/tests/parse_combo_module.rs index 765db716..56d8623a 100644 --- a/xlsynth-vastly/tests/parse_combo_module.rs +++ b/xlsynth-vastly/tests/parse_combo_module.rs @@ -136,6 +136,72 @@ endmodule assert_eq!(out_wrap_zero["out"].to_bit_string_msb_first(), "10000000"); } +#[test] +fn indexed_assign_lhs_supports_bitvector_build_up() { + let dut = r#" +module bitvector_lhs_build_up( + input wire a, + input wire b, + input wire c, + input wire d, + output wire [3:0] out +); + wire [3:0] v; + assign v[0] = a; + assign v[1] = b; + assign v[2] = c; + assign v[3] = d; + assign out = v; +endmodule +"#; + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let out = eval_combo( + &m, + &plan, + &[ + ("a".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("b".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("c".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("d".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out["out"].to_bit_string_msb_first(), "0101"); +} + +#[test] +fn indexed_assign_lhs_supports_packed_array_build_up_with_cast() { + let dut = r#" +module packed_lhs_build_up( + input wire [1:0] lo, + input wire [1:0] hi, + output wire [3:0] out +); + wire [1:0][1:0] p; + assign p[0] = lo; + assign p[1] = hi; + assign out = $unsigned(p); +endmodule +"#; + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let out = eval_combo( + &m, + &plan, + &[ + ("lo".to_string(), vbits(2, Signedness::Unsigned, "10")), + ("hi".to_string(), vbits(2, Signedness::Unsigned, "01")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out["out"].to_bit_string_msb_first(), "0110"); +} + #[test] fn parses_and_evals_assignment_context_sized_ops() { let dut = r#" @@ -932,6 +998,100 @@ endmodule assert_eq!(out["p_down"].to_bit_string_msb_first(), "xxx"); } +#[test] +fn oob_lhs_indexed_assign_is_noop_for_bitvector() { + let dut = r#" +module oob_lhs_bitvector( + output wire [3:0] out +); + wire [3:0] v; + assign v = 4'b1010; + assign v[7] = 1'b1; + assign out = v; +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["out"].to_bit_string_msb_first(), "1010"); +} + +#[test] +fn oob_lhs_indexed_assign_is_noop_for_packed_array() { + let dut = r#" +module oob_lhs_packed( + output wire [7:0] out +); + wire [1:0][3:0] p; + assign p = 8'b10100011; + assign p[2] = 4'b1111; + assign out = p; +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["out"].to_bit_string_msb_first(), "10100011"); +} + +#[test] +fn oob_rhs_index_read_returns_x_for_bitvector() { + let dut = r#" +module oob_rhs_bitvector( + input wire [3:0] a, + input wire [2:0] idx, + output wire y +); + assign y = a[idx]; +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let out = eval_combo( + &m, + &plan, + &[ + ("a".to_string(), vbits(4, Signedness::Unsigned, "1010")), + ("idx".to_string(), vbits(3, Signedness::Unsigned, "100")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out["y"].to_bit_string_msb_first(), "x"); +} + +#[test] +fn oob_rhs_index_read_returns_x_for_packed_array() { + let dut = r#" +module oob_rhs_packed( + input wire [1:0][3:0] a, + input wire [1:0] idx, + output wire [3:0] y +); + assign y = a[idx]; +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let out = eval_combo( + &m, + &plan, + &[ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("idx".to_string(), vbits(2, Signedness::Unsigned, "10")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out["y"].to_bit_string_msb_first(), "xxxx"); +} + #[test] fn coverage_eval_supports_signed_unsigned_cast_builtins() { let dut = r#" @@ -967,6 +1127,49 @@ endmodule ); } +#[test] +fn coverage_eval_matches_plain_eval_for_packed_index_in_function_assign() { + let dut = r#" +module packed_fn_cov( + input logic [1:0][3:0] a, + input logic idx, + output logic [3:0] y +); + function automatic logic [3:0] pick(input logic i); + begin + pick = a[i]; + end + endfunction + assign y = pick(idx); +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let seed: BTreeMap = [ + ("a".to_string(), vbits(8, Signedness::Unsigned, "10100011")), + ("idx".to_string(), vbits(1, Signedness::Unsigned, "1")), + ] + .into_iter() + .collect(); + let out_ref = eval_combo(&m, &plan, &seed).unwrap(); + + let src = SourceText::new(dut.to_string()); + let mut cov = CoverageCounters::default(); + let mut env = xlsynth_vastly::Env::new(); + for (k, v) in &seed { + env.insert(k.clone(), v.clone()); + } + let out_cov = + eval_combo_seeded_with_coverage(&m, &plan, &env, &src, &mut cov, &BTreeMap::new()).unwrap(); + + assert_eq!(out_ref["y"].to_bit_string_msb_first(), "1010"); + assert_eq!( + out_cov["y"].to_bit_string_msb_first(), + out_ref["y"].to_bit_string_msb_first() + ); +} + #[test] fn coverage_eval_preserves_unknown_dynamic_selectors() { let dut = r#"