diff --git a/xlsynth-vastly/src/combo_compile.rs b/xlsynth-vastly/src/combo_compile.rs index 32039c77..fa8495af 100644 --- a/xlsynth-vastly/src/combo_compile.rs +++ b/xlsynth-vastly/src/combo_compile.rs @@ -7,7 +7,6 @@ use crate::Signedness; use crate::Value4; 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; @@ -117,8 +116,7 @@ pub struct CompiledComboModule { } pub fn compile_combo_module(src: &str) -> Result { - let normalized = normalize_generated_unpacked_arrays(src); - let parse_src = normalized.normalized_src.as_str(); + let parse_src = src; let parsed: ComboModule = crate::sv_parser::parse_combo_module(parse_src)?; let items = crate::generate_constructs::elaborate_combo_items( parse_src, @@ -147,10 +145,7 @@ pub fn compile_combo_module(src: &str) -> Result { PortDir::Input => input_ports.push(port), PortDir::Output => output_ports.push(port), } - decls.insert( - normalized.denormalize_ident(&p.name), - decl_info_from_port_decl(p), - ); + decls.insert(p.name.clone(), decl_info_from_port_decl(p)); } let mut functions: BTreeMap = BTreeMap::new(); @@ -159,10 +154,7 @@ pub fn compile_combo_module(src: &str) -> Result { for it in &items { match it { ComboItem::WireDecl(d) => { - decls.insert( - normalized.denormalize_ident(&d.name), - decl_info_from_decl(d), - ); + decls.insert(d.name.clone(), decl_info_from_decl(d)); } ComboItem::Assign { .. } | ComboItem::Function(_) => {} ComboItem::GenerateFor { .. } | ComboItem::GenerateIf { .. } => { @@ -178,13 +170,10 @@ pub fn compile_combo_module(src: &str) -> Result { let rhs_src = rhs_text .as_deref() .unwrap_or_else(|| parse_src[rhs.start..rhs.end].trim()); - let lhs = denormalize_lhs(lhs.clone(), &normalized.placeholder_to_original); - let lhs = rewrite_packed_lhs(lhs, &decls)?; - let rhs_expr = - denormalize_expr(parse_expr(rhs_src)?, &normalized.placeholder_to_original); + let lhs = rewrite_packed_lhs(lhs.clone(), &decls)?; + let rhs_expr = parse_expr(rhs_src)?; 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)?; assigns.push(ComboAssign { @@ -197,52 +186,30 @@ pub fn compile_combo_module(src: &str) -> Result { 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), - ); + fn_decls.insert(arg.name.clone(), decl_info_from_decl(arg)); } for local in &f.locals { - fn_decls.insert( - normalized.denormalize_ident(&local.name), - decl_info_from_decl(local), - ); + fn_decls.insert(local.name.clone(), decl_info_from_decl(local)); } let args: Vec = f.args.iter().map(lower_decl_to_function_var).collect(); let locals: BTreeMap = f .locals .iter() - .map(|d| { - ( - normalized.denormalize_ident(&d.name), - decl_info_from_decl(d), - ) - }) + .map(|d| (d.name.clone(), decl_info_from_decl(d))) .collect(); let body = match &f.body { ComboFunctionBody::UniqueCasez { selector, arms, .. } => { let selector_src = parse_src[selector.start..selector.end].trim(); - let selector_expr = rewrite_packed_expr( - denormalize_expr( - parse_expr(selector_src)?, - &normalized.placeholder_to_original, - ), - &fn_decls, - ); + let selector_expr = + rewrite_packed_expr(parse_expr(selector_src)?, &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 = rewrite_packed_expr( - denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, - ), - &fn_decls, - ); + let value_expr = rewrite_packed_expr(parse_expr(value_src)?, &fn_decls); let value_expr = value_expr?; let pat = a.pat.as_ref().map(|p| CasezPattern { width: p.width, @@ -260,19 +227,9 @@ pub fn compile_combo_module(src: &str) -> Result { } ComboFunctionBody::Assign { value } => { let value_src = parse_src[value.start..value.end].trim(); - let expr = rewrite_packed_expr( - denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, - ), - &fn_decls, - ); + let expr = rewrite_packed_expr(parse_expr(value_src)?, &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 { @@ -284,16 +241,10 @@ 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 = rewrite_packed_expr( - denormalize_expr( - parse_expr(value_src)?, - &normalized.placeholder_to_original, - ), - &fn_decls, - ); + let expr = rewrite_packed_expr(parse_expr(value_src)?, &fn_decls); let expr = expr?; out_assigns.push(FunctionAssign { - lhs: normalized.denormalize_ident(&a.lhs), + lhs: a.lhs.clone(), expr, }); } @@ -313,13 +264,7 @@ pub fn compile_combo_module(src: &str) -> Result { } else { Signedness::Unsigned }, - args: args - .into_iter() - .map(|mut a| { - a.name = normalized.denormalize_ident(&a.name); - a - }) - .collect(), + args, locals, body, }, @@ -351,6 +296,7 @@ fn decl_info_from_decl(d: &Decl) -> DeclInfo { Signedness::Unsigned }, packed_dims: d.packed_dims.clone(), + unpacked_dims: d.unpacked_dims.clone(), } } @@ -363,6 +309,7 @@ fn decl_info_from_port_decl(p: &crate::sv_ast::PortDecl) -> DeclInfo { Signedness::Unsigned }, packed_dims: p.packed_dims.clone(), + unpacked_dims: p.unpacked_dims.clone(), } } @@ -377,710 +324,3 @@ fn lower_decl_to_function_var(d: &Decl) -> FunctionVar { }, } } - -#[derive(Debug, Default)] -struct ArrayNormalization { - normalized_src: String, - placeholder_to_original: BTreeMap, -} - -impl ArrayNormalization { - fn denormalize_ident(&self, name: &str) -> String { - self.placeholder_to_original - .get(name) - .cloned() - .unwrap_or_else(|| name.to_string()) - } -} - -fn normalize_generated_unpacked_arrays(src: &str) -> ArrayNormalization { - let mut decls_by_base = BTreeMap::new(); - let mut replacement_pairs: Vec<(String, String)> = Vec::new(); - let mut out_lines: Vec = Vec::new(); - let lines: Vec<&str> = src.lines().collect(); - let mut i = 0usize; - while i < lines.len() { - let line = lines[i]; - if let Some(decl) = parse_generated_unpacked_array_decl_line(line) { - for indices in decl.full_index_tuples() { - let placeholder = decl.placeholder(&indices); - replacement_pairs.push((decl.original_ref(&indices), placeholder.clone())); - out_lines.push(render_flat_decl_line(&decl, &placeholder)); - } - decls_by_base.insert(decl.base_name.clone(), decl); - i += 1; - continue; - } - - if let Some(loop_info) = parse_generated_genvar_for_header(line) { - if decls_by_base.is_empty() { - // Keep generic generate loops intact when we are not doing - // generated-unpacked-array normalization. - out_lines.push(line.to_string()); - i += 1; - continue; - } - - let Some(j) = find_matching_end_line(&lines, i + 1) else { - out_lines.push(line.to_string()); - i += 1; - continue; - }; - let body_lines = &lines[i + 1..j]; - for iter in loop_info.start..loop_info.limit { - let iter_text = iter.to_string(); - for body_line in body_lines { - let substituted = replace_ident_token(body_line, &loop_info.var, &iter_text); - out_lines.extend(expand_generated_unpacked_array_assign_line( - &substituted, - &decls_by_base, - )); - } - } - i = j + 1; - } else { - out_lines.extend(expand_generated_unpacked_array_assign_line( - line, - &decls_by_base, - )); - i += 1; - } - } - - let mut normalized_src = out_lines.join("\n"); - if src.ends_with('\n') { - normalized_src.push('\n'); - } - replacement_pairs.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len()).then_with(|| a.cmp(b))); - let mut placeholder_to_original = BTreeMap::new(); - for (original, placeholder) in replacement_pairs { - normalized_src = normalized_src.replace(&original, &placeholder); - placeholder_to_original.insert(placeholder, original); - } - - ArrayNormalization { - normalized_src, - placeholder_to_original, - } -} - -#[derive(Debug, Clone)] -struct GeneratedUnpackedArrayDecl { - indent: String, - kind: String, - signed: bool, - packed_range: Option, - base_name: String, - dimensions: Vec>, -} - -impl GeneratedUnpackedArrayDecl { - fn rank(&self) -> usize { - self.dimensions.len() - } - - fn full_index_tuples(&self) -> Vec> { - let mut out = Vec::new(); - let mut prefix = Vec::new(); - collect_index_tuples(&self.dimensions, &mut prefix, &mut out); - out - } - - fn original_ref(&self, indices: &[u32]) -> String { - let mut s = self.base_name.clone(); - for idx in indices { - s.push('['); - s.push_str(&idx.to_string()); - s.push(']'); - } - s - } - - fn placeholder(&self, indices: &[u32]) -> String { - make_array_placeholder(&self.base_name, indices) - } -} - -fn parse_generated_unpacked_array_decl_line(line: &str) -> Option { - let indent_len = line.len() - line.trim_start_matches(char::is_whitespace).len(); - let indent = line[..indent_len].to_string(); - let mut rest = line[indent_len..].trim_end(); - rest = rest.strip_suffix(';')?.trim_end(); - - let (kind, after_kind) = if let Some(r) = rest.strip_prefix("wire ") { - ("wire".to_string(), r) - } else if let Some(r) = rest.strip_prefix("logic ") { - ("logic".to_string(), r) - } else { - return None; - }; - - let (signed, after_signed) = if let Some(r) = after_kind.strip_prefix("signed ") { - (true, r) - } else { - (false, after_kind) - }; - - let (packed_range, after_packed) = if after_signed.starts_with('[') { - let (range, rest) = take_bracket_group(after_signed)?; - (Some(range.to_string()), rest.trim_start()) - } else { - (None, after_signed.trim_start()) - }; - - let name_end = after_packed - .find(|c: char| !(c == '_' || c.is_ascii_alphanumeric())) - .unwrap_or(after_packed.len()); - if name_end == 0 { - return None; - } - let base_name = after_packed[..name_end].to_string(); - let mut after_name = after_packed[name_end..].trim_start(); - if !after_name.starts_with('[') { - return None; - } - let mut dimensions = Vec::new(); - while after_name.starts_with('[') { - let (group, tail) = take_bracket_group(after_name)?; - dimensions.push(parse_unpacked_indices(group)?); - after_name = tail.trim_start(); - } - if !after_name.is_empty() { - return None; - } - - Some(GeneratedUnpackedArrayDecl { - indent, - kind, - signed, - packed_range, - base_name, - dimensions, - }) -} - -fn take_bracket_group(s: &str) -> Option<(&str, &str)> { - if !s.starts_with('[') { - return None; - } - let mut depth = 0u32; - for (idx, ch) in s.char_indices() { - match ch { - '[' => depth += 1, - ']' => { - depth = depth.checked_sub(1)?; - if depth == 0 { - return Some((&s[..=idx], &s[idx + 1..])); - } - } - _ => {} - } - } - None -} - -fn parse_unpacked_indices(group: &str) -> Option> { - let inner = group.strip_prefix('[')?.strip_suffix(']')?.trim(); - if let Some((lhs, rhs)) = inner.split_once(':') { - let start = lhs.trim().parse::().ok()?; - let end = rhs.trim().parse::().ok()?; - if start <= end { - Some((start..=end).collect()) - } else { - Some((end..=start).rev().collect()) - } - } else { - let count = inner.parse::().ok()?; - Some((0..count).collect()) - } -} - -fn render_flat_decl_line(decl: &GeneratedUnpackedArrayDecl, placeholder: &str) -> String { - let mut s = String::new(); - s.push_str(&decl.indent); - s.push_str(&decl.kind); - s.push(' '); - if decl.signed { - s.push_str("signed "); - } - if let Some(range) = &decl.packed_range { - s.push_str(range); - s.push(' '); - } - s.push_str(placeholder); - s.push(';'); - s -} - -fn make_array_placeholder(base: &str, indices: &[u32]) -> String { - let mut s = String::from(base); - for idx in indices { - s.push_str("__vastly_idx_"); - s.push_str(&idx.to_string()); - } - s -} - -fn collect_index_tuples(dims: &[Vec], prefix: &mut Vec, out: &mut Vec>) { - if dims.is_empty() { - out.push(prefix.clone()); - return; - } - for &idx in &dims[0] { - prefix.push(idx); - collect_index_tuples(&dims[1..], prefix, out); - prefix.pop(); - } -} - -fn expand_generated_unpacked_array_assign_line( - line: &str, - decls_by_base: &BTreeMap, -) -> Vec { - let Some((indent, lhs, rhs)) = parse_generated_assign_line(line) else { - return vec![line.to_string()]; - }; - let (_lhs_base, lhs_prefix, lhs_decl) = - match parse_array_ref(&lhs).and_then(|(lhs_base, lhs_prefix)| { - decls_by_base - .get(&lhs_base) - .map(|d| (lhs_base, lhs_prefix, d)) - }) { - Some(found) => found, - None => { - let rhs_ref = rewrite_array_refs_for_suffix(&rhs, decls_by_base, &[]); - if rhs_ref == rhs { - return vec![line.to_string()]; - } - return vec![format!("{indent}assign {lhs} = {rhs_ref};")]; - } - }; - if lhs_prefix.len() > lhs_decl.rank() { - return vec![line.to_string()]; - } - - let suffix_dims = &lhs_decl.dimensions[lhs_prefix.len()..]; - if suffix_dims.is_empty() { - let rhs_ref = rewrite_array_refs_for_suffix(&rhs, decls_by_base, &[]); - if rhs_ref == rhs { - return vec![line.to_string()]; - } - return vec![format!("{indent}assign {lhs} = {rhs_ref};")]; - } - - let mut suffixes = Vec::new(); - collect_index_tuples(suffix_dims, &mut Vec::new(), &mut suffixes); - let mut out = Vec::with_capacity(suffixes.len()); - for suffix in suffixes { - let mut full_lhs = lhs_prefix.clone(); - full_lhs.extend_from_slice(&suffix); - let lhs_ref = lhs_decl.original_ref(&full_lhs); - let rhs_ref = rewrite_array_refs_for_suffix(&rhs, decls_by_base, &suffix); - out.push(format!("{indent}assign {lhs_ref} = {rhs_ref};")); - } - out -} - -fn parse_generated_assign_line(line: &str) -> Option<(String, String, String)> { - let indent_len = line.len() - line.trim_start_matches(char::is_whitespace).len(); - let indent = line[..indent_len].to_string(); - let mut rest = line[indent_len..].trim_end(); - rest = rest.strip_suffix(';')?.trim_end(); - let rest = rest.strip_prefix("assign ")?; - let eq = rest.find(" = ")?; - let lhs = rest[..eq].trim().to_string(); - let rhs = rest[eq + 3..].trim().to_string(); - Some((indent, lhs, rhs)) -} - -fn parse_array_ref(s: &str) -> Option<(String, Vec)> { - let mut chars = s.char_indices(); - let (_, first) = chars.next()?; - if !(first == '_' || first.is_ascii_alphabetic()) { - return None; - } - let mut name_end = first.len_utf8(); - for (idx, ch) in chars { - if ch == '_' || ch.is_ascii_alphanumeric() { - name_end = idx + ch.len_utf8(); - } else { - break; - } - } - let base = s[..name_end].to_string(); - let mut rest = &s[name_end..]; - let mut indices = Vec::new(); - while rest.starts_with('[') { - let (group, tail) = take_bracket_group(rest)?; - let inner = group.strip_prefix('[')?.strip_suffix(']')?.trim(); - indices.push(inner.parse::().ok()?); - rest = tail; - } - if !rest.is_empty() { - return None; - } - Some((base, indices)) -} - -fn rewrite_array_refs_for_suffix( - expr: &str, - decls_by_base: &BTreeMap, - suffix: &[u32], -) -> String { - let bytes = expr.as_bytes(); - let mut out = String::with_capacity(expr.len()); - let mut i = 0usize; - while i < bytes.len() { - let c = bytes[i]; - if is_ident_start(c) { - let start = i; - i += 1; - while i < bytes.len() && is_ident_continue(bytes[i]) { - i += 1; - } - let base = &expr[start..i]; - if let Some(decl) = decls_by_base.get(base) { - let mut full = String::from(base); - let mut groups: Vec = Vec::new(); - let mut j = i; - while let Some((group, tail)) = take_bracket_group(&expr[j..]) { - groups.push(group.to_string()); - full.push_str(group); - j = expr.len() - tail.len(); - } - if let Some(lowered) = lower_array_ref(decl, base, &groups, suffix) { - out.push_str(&lowered); - i = j; - continue; - } - out.push_str(&full); - i = j; - continue; - } - out.push_str(base); - continue; - } - out.push(c as char); - i += 1; - } - out -} - -#[derive(Debug)] -struct GeneratedGenvarLoop { - var: String, - start: u32, - limit: u32, -} - -fn parse_generated_genvar_for_header(line: &str) -> Option { - let trimmed = line.trim(); - let rest = trimmed.strip_prefix("for (genvar ")?; - let var_end = rest - .find(|c: char| !(c == '_' || c.is_ascii_alphanumeric())) - .unwrap_or(rest.len()); - if var_end == 0 { - return None; - } - let var = rest[..var_end].to_string(); - let rest = &rest[var_end..]; - let rest = rest.strip_prefix(" = ")?; - let (start_text, rest) = rest.split_once(';')?; - let start = start_text.trim().parse::().ok()?; - let rest = rest.trim_start(); - let rest = rest.strip_prefix(&var)?; - let rest = rest.strip_prefix(" < ")?; - let (limit_text, rest) = rest.split_once(';')?; - let limit = limit_text.trim().parse::().ok()?; - let rest = rest.trim_start(); - let rest = rest.strip_prefix(&var)?; - let rest = rest.strip_prefix(" = ")?; - let rest = rest.strip_prefix(&var)?; - let rest = rest.strip_prefix(" + 1) begin")?; - if !rest.trim_start().starts_with(':') { - return None; - } - Some(GeneratedGenvarLoop { var, start, limit }) -} - -fn find_matching_end_line(lines: &[&str], start: usize) -> Option { - let mut depth: i32 = 1; - let mut i = start; - while i < lines.len() { - let line = strip_line_comment(lines[i]); - let begin_count = count_keyword_token(line, "begin") as i32; - let end_count = count_keyword_token(line, "end") as i32; - depth += begin_count - end_count; - if depth == 0 { - return Some(i); - } - if depth < 0 { - return None; - } - i += 1; - } - None -} - -fn strip_line_comment(line: &str) -> &str { - line.split_once("//").map_or(line, |(prefix, _)| prefix) -} - -fn count_keyword_token(s: &str, keyword: &str) -> usize { - let bytes = s.as_bytes(); - let mut i = 0usize; - let mut count = 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; - } - if &s[start..i] == keyword { - count += 1; - } - } else { - i += 1; - } - } - count -} - -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 lower_array_ref( - decl: &GeneratedUnpackedArrayDecl, - _base: &str, - groups: &[String], - suffix: &[u32], -) -> Option { - let mut numeric_prefix = Vec::new(); - let mut dynamic_expr: Option<&str> = None; - let mut trailing_numeric = Vec::new(); - - for group in groups { - let inner = group.strip_prefix('[')?.strip_suffix(']')?.trim(); - if dynamic_expr.is_none() { - if let Ok(idx) = inner.parse::() { - numeric_prefix.push(idx); - } else { - dynamic_expr = Some(inner); - } - } else if let Ok(idx) = inner.parse::() { - trailing_numeric.push(idx); - } else { - return None; - } - } - - if dynamic_expr.is_none() { - if numeric_prefix.len() > decl.rank() { - return None; - } - let remaining_rank = decl.rank() - numeric_prefix.len(); - if remaining_rank != suffix.len() { - return None; - } - let mut indices = numeric_prefix; - indices.extend_from_slice(suffix); - return Some(decl.original_ref(&indices)); - } - - let dyn_expr = dynamic_expr?; - let dyn_dim = decl.dimensions.get(numeric_prefix.len())?; - let resolved_len = numeric_prefix.len() + 1 + trailing_numeric.len() + suffix.len(); - if resolved_len != decl.rank() { - return None; - } - - let mut fallback = String::from("'x"); - for &candidate in dyn_dim.iter().rev() { - let mut indices = numeric_prefix.clone(); - indices.push(candidate); - indices.extend_from_slice(&trailing_numeric); - indices.extend_from_slice(suffix); - let candidate_ref = decl.original_ref(&indices); - fallback = format!( - "(({}) === {} ? {} : {})", - dyn_expr, candidate, candidate_ref, fallback - ); - } - Some(fallback) -} - -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() -} - -fn denormalize_expr(expr: Expr, placeholders: &BTreeMap) -> Expr { - match expr { - Expr::Ident(name) => Expr::Ident(placeholders.get(&name).cloned().unwrap_or(name)), - Expr::Literal(v) => Expr::Literal(v), - Expr::UnsizedNumber(v) => Expr::UnsizedNumber(v), - Expr::UnbasedUnsized(b) => Expr::UnbasedUnsized(b), - Expr::Call { name, args } => Expr::Call { - name, - args: args - .into_iter() - .map(|a| denormalize_expr(a, placeholders)) - .collect(), - }, - Expr::Concat(parts) => Expr::Concat( - parts - .into_iter() - .map(|p| denormalize_expr(p, placeholders)) - .collect(), - ), - Expr::Replicate { count, expr } => Expr::Replicate { - count: Box::new(denormalize_expr(*count, placeholders)), - expr: Box::new(denormalize_expr(*expr, placeholders)), - }, - Expr::Cast { width, expr } => Expr::Cast { - width: Box::new(denormalize_expr(*width, placeholders)), - expr: Box::new(denormalize_expr(*expr, placeholders)), - }, - Expr::Index { expr, index } => Expr::Index { - expr: Box::new(denormalize_expr(*expr, placeholders)), - index: Box::new(denormalize_expr(*index, placeholders)), - }, - Expr::Slice { expr, msb, lsb } => Expr::Slice { - expr: Box::new(denormalize_expr(*expr, placeholders)), - msb: Box::new(denormalize_expr(*msb, placeholders)), - lsb: Box::new(denormalize_expr(*lsb, placeholders)), - }, - Expr::IndexedSlice { - expr, - base, - width, - upward, - } => Expr::IndexedSlice { - expr: Box::new(denormalize_expr(*expr, placeholders)), - base: Box::new(denormalize_expr(*base, placeholders)), - width: Box::new(denormalize_expr(*width, placeholders)), - upward, - }, - Expr::Unary { op, expr } => Expr::Unary { - op, - expr: Box::new(denormalize_expr(*expr, placeholders)), - }, - Expr::Binary { op, lhs, rhs } => Expr::Binary { - op, - lhs: Box::new(denormalize_expr(*lhs, placeholders)), - rhs: Box::new(denormalize_expr(*rhs, placeholders)), - }, - Expr::Ternary { cond, t, f } => Expr::Ternary { - cond: Box::new(denormalize_expr(*cond, placeholders)), - t: Box::new(denormalize_expr(*t, placeholders)), - f: Box::new(denormalize_expr(*f, placeholders)), - }, - } -} - -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) => { - if let Some(original) = placeholders.get(name) { - *name = original.clone(); - } - } - SpannedExprKind::Literal(_) - | SpannedExprKind::UnsizedNumber(_) - | SpannedExprKind::UnbasedUnsized(_) => {} - SpannedExprKind::Call { args, .. } => { - for a in args { - denormalize_spanned_expr(a, placeholders); - } - } - SpannedExprKind::Concat(parts) => { - for p in parts { - denormalize_spanned_expr(p, placeholders); - } - } - SpannedExprKind::Replicate { count, expr } => { - denormalize_spanned_expr(count, placeholders); - denormalize_spanned_expr(expr, placeholders); - } - SpannedExprKind::Cast { width, expr } => { - denormalize_spanned_expr(width, placeholders); - denormalize_spanned_expr(expr, placeholders); - } - SpannedExprKind::Index { expr, index } => { - denormalize_spanned_expr(expr, placeholders); - denormalize_spanned_expr(index, placeholders); - } - SpannedExprKind::Slice { expr, msb, lsb } => { - denormalize_spanned_expr(expr, placeholders); - denormalize_spanned_expr(msb, placeholders); - denormalize_spanned_expr(lsb, placeholders); - } - SpannedExprKind::IndexedSlice { - expr, base, width, .. - } => { - denormalize_spanned_expr(expr, placeholders); - denormalize_spanned_expr(base, placeholders); - denormalize_spanned_expr(width, placeholders); - } - SpannedExprKind::Unary { expr, .. } => { - denormalize_spanned_expr(expr, placeholders); - } - SpannedExprKind::Binary { lhs, rhs, .. } => { - denormalize_spanned_expr(lhs, placeholders); - denormalize_spanned_expr(rhs, placeholders); - } - SpannedExprKind::Ternary { cond, t, f } => { - denormalize_spanned_expr(cond, placeholders); - denormalize_spanned_expr(t, placeholders); - denormalize_spanned_expr(f, placeholders); - } - } -} diff --git a/xlsynth-vastly/src/combo_eval.rs b/xlsynth-vastly/src/combo_eval.rs index ccd17aab..bb802528 100644 --- a/xlsynth-vastly/src/combo_eval.rs +++ b/xlsynth-vastly/src/combo_eval.rs @@ -782,6 +782,7 @@ fn function_return_decl(f: &ComboFunction) -> crate::module_compile::DeclInfo { width: f.ret_width, signedness: f.ret_signedness, packed_dims: vec![f.ret_width], + unpacked_dims: vec![], } } @@ -979,6 +980,7 @@ fn init_function_env(f: &ComboFunction, args: &[Value4], globals: &Env) -> Env { width: arg.width, signedness: arg.signedness, packed_dims: vec![arg.width], + unpacked_dims: vec![], }; env.insert(arg.name.clone(), coerce_to_declinfo(av, &info)); } @@ -1004,6 +1006,7 @@ fn function_target_decl(f: &ComboFunction, lhs: &str) -> Option, + pub unpacked_dims: Vec, } pub fn compile_module(src: &str) -> Result { @@ -42,6 +43,7 @@ pub fn compile_module(src: &str) -> Result { signed, width, packed_dims, + unpacked_dims, } in m.decls.clone() { let signedness = if signed { @@ -55,6 +57,7 @@ pub fn compile_module(src: &str) -> Result { width, signedness, packed_dims, + unpacked_dims, }, ); } diff --git a/xlsynth-vastly/src/packed.rs b/xlsynth-vastly/src/packed.rs index 6e44e2f0..f5e35802 100644 --- a/xlsynth-vastly/src/packed.rs +++ b/xlsynth-vastly/src/packed.rs @@ -43,6 +43,13 @@ fn remaining_width_after(dims: &[u32], consumed_indices: usize) -> Result { } } +fn selection_dims(info: &DeclInfo) -> Vec { + let mut dims = Vec::with_capacity(info.unpacked_dims.len() + info.packed_dims.len()); + dims.extend_from_slice(&info.unpacked_dims); + dims.extend_from_slice(&info.packed_dims); + dims +} + fn build_linear_index( indices: &[T], packed_dims: &[u32], @@ -205,13 +212,12 @@ fn lower_expr_index_chain( 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 dims = selection_dims(info); + if indices.len() > dims.len() { + return Err(Error::Parse(format!("too many 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 linear_index = build_linear_index(&indices, &dims, mul_expr, add_expr)?; + let remaining_width = remaining_width_after(&dims, indices.len())?; let base_expr = Expr::Ident(base_name.to_string()); if remaining_width == 1 { Ok(Some(Expr::Index { @@ -243,18 +249,17 @@ fn lower_spanned_index_chain( 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 dims = selection_dims(info); + if indices.len() > dims.len() { + return Err(Error::Parse(format!("too many indices for `{base_name}`"))); } let linear_index = build_linear_index( &indices, - &info.packed_dims, + &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())?; + let remaining_width = remaining_width_after(&dims, indices.len())?; if remaining_width == 1 { Ok(Some(SpannedExpr { span: expr_span, @@ -528,13 +533,12 @@ pub fn packed_index_selection(info: &DeclInfo, indices: &[u32]) -> Result<(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 dims = selection_dims(info); + if indices.len() > dims.len() { + return Err(Error::Parse("too many indices for declaration".to_string())); } - let offset = checked_packed_offset(&info.packed_dims, indices)?; - let width = remaining_width_after(&info.packed_dims, indices.len())?; + let offset = checked_packed_offset(&dims, indices)?; + let width = remaining_width_after(&dims, indices.len())?; Ok((offset, width)) } @@ -545,14 +549,13 @@ pub fn packed_index_selection_if_in_bounds( 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 dims = selection_dims(info); + if indices.len() > dims.len() { + return Err(Error::Parse("too many indices for declaration".to_string())); } - let Some(offset) = checked_packed_offset_if_in_bounds(&info.packed_dims, indices)? else { + let Some(offset) = checked_packed_offset_if_in_bounds(&dims, indices)? else { return Ok(None); }; - let width = remaining_width_after(&info.packed_dims, indices.len())?; + let width = remaining_width_after(&dims, indices.len())?; Ok(Some((offset, width))) } diff --git a/xlsynth-vastly/src/pipeline_compile.rs b/xlsynth-vastly/src/pipeline_compile.rs index a7323362..b38b7123 100644 --- a/xlsynth-vastly/src/pipeline_compile.rs +++ b/xlsynth-vastly/src/pipeline_compile.rs @@ -87,10 +87,14 @@ pub fn compile_pipeline_module_with_defines( src: &str, defines: &BTreeSet, ) -> Result { + let parse_src = src; let parsed: PipelineModule = - crate::sv_parser::parse_pipeline_module_with_defines(src, defines)?; - let items = - crate::generate_constructs::elaborate_pipeline_items(src, &parsed.params, &parsed.items)?; + 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, + )?; let module_name = parsed.name.clone(); @@ -147,7 +151,7 @@ pub fn compile_pipeline_module_with_defines( } => { let rhs_src = rhs_text .as_deref() - .unwrap_or_else(|| src[rhs.start..rhs.end].trim()); + .unwrap_or_else(|| parse_src[rhs.start..rhs.end].trim()); let lhs = rewrite_packed_lhs(lhs.clone(), &decls)?; 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)?; @@ -204,7 +208,7 @@ pub fn compile_pipeline_module_with_defines( let body = match &f.body { crate::sv_ast::ComboFunctionBody::UniqueCasez { selector, arms, .. } => { - let selector_src = src[selector.start..selector.end].trim(); + let selector_src = parse_src[selector.start..selector.end].trim(); let selector_expr = rewrite_packed_expr( crate::parser::parse_expr(selector_src)?, &fn_decls, @@ -212,7 +216,7 @@ pub fn compile_pipeline_module_with_defines( let mut out_arms: Vec = Vec::new(); for a in arms { - let value_src = src[a.value.start..a.value.end].trim(); + let value_src = parse_src[a.value.start..a.value.end].trim(); let value_expr = rewrite_packed_expr( crate::parser::parse_expr(value_src)?, &fn_decls, @@ -232,7 +236,7 @@ pub fn compile_pipeline_module_with_defines( } } crate::sv_ast::ComboFunctionBody::Assign { value } => { - let value_src = src[value.start..value.end].trim(); + let value_src = parse_src[value.start..value.end].trim(); let expr = rewrite_packed_expr(crate::parser::parse_expr(value_src)?, &fn_decls)?; let mut expr_spanned = @@ -248,7 +252,7 @@ pub fn compile_pipeline_module_with_defines( let mut out_assigns: Vec = Vec::with_capacity(assigns.len()); for a in assigns { - let value_src = src[a.value.start..a.value.end].trim(); + let value_src = parse_src[a.value.start..a.value.end].trim(); let expr = rewrite_packed_expr( crate::parser::parse_expr(value_src)?, &fn_decls, @@ -506,6 +510,7 @@ fn decl_info_from_decl(d: &crate::sv_ast::Decl) -> DeclInfo { Signedness::Unsigned }, packed_dims: d.packed_dims.clone(), + unpacked_dims: d.unpacked_dims.clone(), } } @@ -518,6 +523,7 @@ fn decl_info_from_port_decl(p: &crate::sv_ast::PortDecl) -> DeclInfo { Signedness::Unsigned }, packed_dims: p.packed_dims.clone(), + unpacked_dims: p.unpacked_dims.clone(), } } diff --git a/xlsynth-vastly/src/sv_ast.rs b/xlsynth-vastly/src/sv_ast.rs index 9650c0c4..28bce04d 100644 --- a/xlsynth-vastly/src/sv_ast.rs +++ b/xlsynth-vastly/src/sv_ast.rs @@ -29,6 +29,7 @@ pub struct PortDecl { pub signed: bool, pub width: u32, pub packed_dims: Vec, + pub unpacked_dims: Vec, pub name: String, } @@ -128,6 +129,7 @@ pub struct Decl { pub signed: bool, pub width: u32, pub packed_dims: Vec, + pub unpacked_dims: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/xlsynth-vastly/src/sv_parser.rs b/xlsynth-vastly/src/sv_parser.rs index a3bbe0a8..2dc845b0 100644 --- a/xlsynth-vastly/src/sv_parser.rs +++ b/xlsynth-vastly/src/sv_parser.rs @@ -900,12 +900,15 @@ impl<'a> Parser<'a> { } _ => return Err(Error::Parse("expected port identifier".to_string())), }; + let unpacked_dims = self.parse_unpacked_dims()?; + let width = dims_total_width(width, &unpacked_dims)?; decls.push(Decl { name, signed, width, packed_dims, + unpacked_dims, }); match self.cur() { @@ -978,6 +981,8 @@ impl<'a> Parser<'a> { } _ => return Err(Error::Parse("expected port identifier".to_string())), }; + let unpacked_dims = self.parse_unpacked_dims()?; + let width = dims_total_width(width, &unpacked_dims)?; ports.push(PortDecl { dir, @@ -985,6 +990,7 @@ impl<'a> Parser<'a> { signed, width, packed_dims, + unpacked_dims, name, }); @@ -1010,6 +1016,11 @@ impl<'a> Parser<'a> { self.expect(TokKind::RBracket)?; let msb = self.eval_const_u32(&msb_expr)?; let lsb = self.eval_const_u32(&lsb_expr)?; + if lsb != 0 { + return Err(Error::Parse( + "packed declaration ranges must be zero-based `[N:0]`".to_string(), + )); + } if msb < lsb { return Err(Error::Parse("decl range msb Parser<'a> { Ok(dims) } + fn parse_unpacked_dims(&mut self) -> Result> { + let mut dims: Vec = Vec::new(); + while *self.cur() == TokKind::LBracket { + self.bump(); + let first = self.parse_expr_until(&[TokKind::Colon, TokKind::RBracket])?; + let dim = if *self.cur() == TokKind::Colon { + self.bump(); + let second = self.parse_expr_until(&[TokKind::RBracket])?; + let first_u = self.eval_const_u32(&first)?; + let second_u = self.eval_const_u32(&second)?; + if second_u != 0 { + return Err(Error::Parse( + "unpacked declaration ranges must be zero-based `[N:0]`".to_string(), + )); + } + first_u + 1 + } else { + self.eval_const_u32(&first)? + }; + self.expect(TokKind::RBracket)?; + if dim == 0 { + return Err(Error::Parse( + "unpacked array dimension must be > 0".to_string(), + )); + } + dims.push(dim); + } + Ok(dims) + } + fn parse_wire_decl(&mut self) -> Result { let (d, _span) = self.parse_wire_decl_with_span()?; Ok(d) @@ -1047,6 +1088,8 @@ impl<'a> Parser<'a> { } _ => return Err(Error::Parse("expected identifier in wire decl".to_string())), }; + let unpacked_dims = self.parse_unpacked_dims()?; + let width = dims_total_width(width, &unpacked_dims)?; self.expect(TokKind::Semi)?; let end = self.toks[self.idx - 1].end; Ok(( @@ -1055,6 +1098,7 @@ impl<'a> Parser<'a> { signed, width, packed_dims, + unpacked_dims, }, Span { start, end }, )) @@ -1271,6 +1315,8 @@ impl<'a> Parser<'a> { } _ => return Err(Error::Parse("expected identifier in decl".to_string())), }; + let unpacked_dims = self.parse_unpacked_dims()?; + let width = dims_total_width(width, &unpacked_dims)?; if expect_semi { self.expect(TokKind::Semi)?; } @@ -1279,6 +1325,7 @@ impl<'a> Parser<'a> { signed, width, packed_dims, + unpacked_dims, }) } @@ -1418,12 +1465,15 @@ impl<'a> Parser<'a> { } _ => return Err(Error::Parse("expected identifier in decl".to_string())), }; + let unpacked_dims = self.parse_unpacked_dims()?; + let width = dims_total_width(width, &unpacked_dims)?; self.expect(TokKind::Semi)?; Ok(Decl { name, signed, width, packed_dims, + unpacked_dims, }) } @@ -1693,6 +1743,13 @@ fn packed_dims_width(dims: &[u32]) -> Result { }) } +fn dims_total_width(packed_width: u32, unpacked_dims: &[u32]) -> Result { + unpacked_dims.iter().try_fold(packed_width, |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/compare_combo_vcd_to_reference_sim.rs b/xlsynth-vastly/tests/compare_combo_vcd_to_reference_sim.rs index 18c0c4f6..c81abaea 100644 --- a/xlsynth-vastly/tests/compare_combo_vcd_to_reference_sim.rs +++ b/xlsynth-vastly/tests/compare_combo_vcd_to_reference_sim.rs @@ -794,7 +794,7 @@ module fuzz_codegen_v( ); wire [2:0] tuple_11; wire [5:0] tuple_13; - wire [2:0] array_12[0:1]; + wire [2:0] array_12[1:0]; wire [38:0] tuple_15; wire [2:0] tuple_index_16; wire [10:0] zero_ext_17; @@ -851,9 +851,9 @@ module fuzz_codegen_v( input wire [1:0] p0, output wire [1:0] out ); - wire [1:0] array_10[0:1]; - wire [1:0] array_11[0:1][0:1]; - wire [1:0] sel_12[0:1][0:1]; + wire [1:0] array_10[1:0]; + wire [1:0] array_11[1:0][1:0]; + wire [1:0] sel_12[1:0][1:0]; assign array_10[0] = p0; assign array_10[1] = ~p0; assign array_11[0][0] = array_10[0]; diff --git a/xlsynth-vastly/tests/packed_arrays.rs b/xlsynth-vastly/tests/packed_arrays.rs index 1586a564..10d7f249 100644 --- a/xlsynth-vastly/tests/packed_arrays.rs +++ b/xlsynth-vastly/tests/packed_arrays.rs @@ -157,3 +157,447 @@ endmodule "1101" ); } + +#[test] +fn pipeline_module_supports_unpacked_array_of_packed_elements() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0] in_data, + output logic [1:0] out_data, + output logic [3:0] snapshot +); + logic [1:0] lanes[1:0]; + logic [1:0] q; + + assign lanes[0] = in_data; + assign lanes[1] = ~in_data; + assign snapshot = {lanes[1], lanes[0]}; + + always_ff @(posedge clk) begin + if (rst) begin + q <= 2'b00; + end else begin + q <= lanes[1]; + end + end + + assign out_data = q; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ] + .into_iter() + .collect(), + }, + ], + }; + + let init: State = BTreeMap::new(); + let outputs = run_pipeline_and_collect_outputs(&m, &stimulus, &init).unwrap(); + assert_eq!( + outputs[0] + .get("snapshot") + .unwrap() + .to_bit_string_msb_first(), + "1001" + ); + assert_eq!( + outputs[0] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "00" + ); + assert_eq!( + outputs[1] + .get("snapshot") + .unwrap() + .to_bit_string_msb_first(), + "1001" + ); + assert_eq!( + outputs[1] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "10" + ); +} + +#[test] +fn pipeline_module_supports_nested_unpacked_arrays_of_packed_elements() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0] in_data, + output logic [1:0] out_data, + output logic [3:0] tap +); + logic [1:0] table[1:0][1:0]; + logic [1:0] pick; + logic [1:0] q; + + assign table[0][0] = in_data; + assign table[0][1] = ~in_data; + assign table[1][0] = 2'b11; + assign table[1][1] = 2'b00; + assign pick = table[0][1]; + assign tap = {table[1][0], table[0][1]}; + + always_ff @(posedge clk) begin + if (rst) begin + q <= 2'b00; + end else begin + q <= pick; + end + end + + assign out_data = q; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ] + .into_iter() + .collect(), + }, + ], + }; + + let init: State = BTreeMap::new(); + let outputs = run_pipeline_and_collect_outputs(&m, &stimulus, &init).unwrap(); + assert_eq!( + outputs[0].get("tap").unwrap().to_bit_string_msb_first(), + "1110" + ); + assert_eq!( + outputs[0] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "00" + ); + assert_eq!( + outputs[1].get("tap").unwrap().to_bit_string_msb_first(), + "1110" + ); + assert_eq!( + outputs[1] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "10" + ); +} + +#[test] +fn combo_module_supports_dynamic_index_on_unpacked_array_of_packed_values() { + let dut = r#" +module m( + input logic [1:0][3:0] a0, + input logic [1:0][3:0] a1, + input logic sel, + output logic [1:0][3:0] y +); + logic [1:0][3:0] arr[1:0]; + assign arr[0] = a0; + assign arr[1] = a1; + assign y = arr[sel]; +endmodule +"#; + + let m = compile_combo_module(dut).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + + let out_sel0 = eval_combo( + &m, + &plan, + &[ + ("a0".to_string(), vbits(8, Signedness::Unsigned, "00111100")), + ("a1".to_string(), vbits(8, Signedness::Unsigned, "10100101")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out_sel0["y"].to_bit_string_msb_first(), "00111100"); + + let out_sel1 = eval_combo( + &m, + &plan, + &[ + ("a0".to_string(), vbits(8, Signedness::Unsigned, "00111100")), + ("a1".to_string(), vbits(8, Signedness::Unsigned, "10100101")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "1")), + ] + .into_iter() + .collect::>(), + ) + .unwrap(); + assert_eq!(out_sel1["y"].to_bit_string_msb_first(), "10100101"); +} + +#[test] +fn pipeline_module_supports_dynamic_index_on_unpacked_array_of_packed_values() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0][3:0] a0, + input logic [1:0][3:0] a1, + input logic sel, + output logic [1:0][3:0] out_data +); + logic [1:0][3:0] arr[1:0]; + logic [1:0][3:0] q; + + assign arr[0] = a0; + assign arr[1] = a1; + + always_ff @(posedge clk) begin + if (rst) begin + q <= 8'h00; + end else begin + q <= arr[sel]; + end + end + + assign out_data = q; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("a0".to_string(), vbits(8, Signedness::Unsigned, "00111100")), + ("a1".to_string(), vbits(8, Signedness::Unsigned, "10100101")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("a0".to_string(), vbits(8, Signedness::Unsigned, "00111100")), + ("a1".to_string(), vbits(8, Signedness::Unsigned, "10100101")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "0")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("a0".to_string(), vbits(8, Signedness::Unsigned, "00111100")), + ("a1".to_string(), vbits(8, Signedness::Unsigned, "10100101")), + ("sel".to_string(), vbits(1, Signedness::Unsigned, "1")), + ] + .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(), + "00000000" + ); + assert_eq!( + outputs[1] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "00111100" + ); + assert_eq!( + outputs[2] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "10100101" + ); +} + +#[test] +fn pipeline_oob_lhs_indexed_write_is_noop_for_unpacked_array_of_packed_values() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0] in0, + input logic [1:0] in1, + output logic [3:0] snapshot +); + logic [1:0] lanes[1:0]; + + always_ff @(posedge clk) begin + if (rst) begin + lanes[0] <= in0; + lanes[1] <= in1; + end else begin + lanes[2] <= 2'b11; + end + end + + assign snapshot = {lanes[1], lanes[0]}; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("in0".to_string(), vbits(2, Signedness::Unsigned, "01")), + ("in1".to_string(), vbits(2, Signedness::Unsigned, "10")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("in0".to_string(), vbits(2, Signedness::Unsigned, "00")), + ("in1".to_string(), vbits(2, Signedness::Unsigned, "00")), + ] + .into_iter() + .collect(), + }, + ], + }; + + let init: State = BTreeMap::new(); + let outputs = run_pipeline_and_collect_outputs(&m, &stimulus, &init).unwrap(); + assert_eq!( + outputs[0] + .get("snapshot") + .unwrap() + .to_bit_string_msb_first(), + "1001" + ); + assert_eq!( + outputs[1] + .get("snapshot") + .unwrap() + .to_bit_string_msb_first(), + "1001" + ); +} + +#[test] +fn pipeline_oob_rhs_index_read_returns_x_for_unpacked_array_of_packed_values() { + let dut = r#" +module m( + input logic clk, + input logic rst, + input logic [1:0] in_data, + input logic [1:0] sel, + output logic [1:0] out_data +); + logic [1:0] lanes[1:0]; + logic [1:0] q; + + assign lanes[0] = in_data; + assign lanes[1] = ~in_data; + + always_ff @(posedge clk) begin + if (rst) begin + q <= 2'b00; + end else begin + q <= lanes[sel]; + end + end + + assign out_data = q; +endmodule +"#; + + let m = compile_pipeline_module(dut).unwrap(); + let stimulus = PipelineStimulus { + half_period: 5, + cycles: vec![ + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "1")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ("sel".to_string(), vbits(2, Signedness::Unsigned, "00")), + ] + .into_iter() + .collect(), + }, + PipelineCycle { + inputs: [ + ("rst".to_string(), vbits(1, Signedness::Unsigned, "0")), + ("in_data".to_string(), vbits(2, Signedness::Unsigned, "01")), + ("sel".to_string(), vbits(2, Signedness::Unsigned, "10")), + ] + .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(), + "00" + ); + assert_eq!( + outputs[1] + .get("out_data") + .unwrap() + .to_bit_string_msb_first(), + "xx" + ); +} diff --git a/xlsynth-vastly/tests/parameterized_module.rs b/xlsynth-vastly/tests/parameterized_module.rs index 78d2c399..c68c3fd1 100644 --- a/xlsynth-vastly/tests/parameterized_module.rs +++ b/xlsynth-vastly/tests/parameterized_module.rs @@ -115,6 +115,64 @@ endmodule ); } +#[test] +fn function_indexing_matches_between_plain_and_coverage_eval() { + let sv = r#" +module combo_fn_index( + input logic [1:0][3:0] in_data, + input logic sel, + output logic [3:0] y +); + function automatic logic [3:0] pick(input logic [1:0][3:0] data, input logic s); + begin + pick = data[s]; + end + endfunction + assign y = pick(in_data, sel); +endmodule +"#; + + let m = compile_combo_module(sv).unwrap(); + let plan = plan_combo_eval(&m).unwrap(); + let src = SourceText::new(sv.to_string()); + + let inputs0 = BTreeMap::from([ + ("in_data".to_string(), ubits(8, "163")), + ("sel".to_string(), ubits(1, "0")), + ]); + let plain0 = eval_combo(&m, &plan, &inputs0).unwrap(); + assert_eq!(plain0.get("y").unwrap().to_bit_string_msb_first(), "0011"); + let mut cov0 = CoverageCounters::default(); + let mut seed0 = Env::new(); + seed0.insert("in_data".to_string(), ubits(8, "163")); + seed0.insert("sel".to_string(), ubits(1, "0")); + let covered0 = + eval_combo_seeded_with_coverage(&m, &plan, &seed0, &src, &mut cov0, &BTreeMap::new()) + .unwrap(); + assert_eq!( + covered0.get("y").unwrap().to_bit_string_msb_first(), + plain0.get("y").unwrap().to_bit_string_msb_first() + ); + + let inputs1 = BTreeMap::from([ + ("in_data".to_string(), ubits(8, "163")), + ("sel".to_string(), ubits(1, "1")), + ]); + let plain1 = eval_combo(&m, &plan, &inputs1).unwrap(); + assert_eq!(plain1.get("y").unwrap().to_bit_string_msb_first(), "1010"); + let mut cov1 = CoverageCounters::default(); + let mut seed1 = Env::new(); + seed1.insert("in_data".to_string(), ubits(8, "163")); + seed1.insert("sel".to_string(), ubits(1, "1")); + let covered1 = + eval_combo_seeded_with_coverage(&m, &plan, &seed1, &src, &mut cov1, &BTreeMap::new()) + .unwrap(); + assert_eq!( + covered1.get("y").unwrap().to_bit_string_msb_first(), + plain1.get("y").unwrap().to_bit_string_msb_first() + ); +} + #[test] fn compile_and_eval_parameterized_pipeline_module_with_pipeline_api() { let sv = r#" diff --git a/xlsynth-vastly/tests/parse_combo_module.rs b/xlsynth-vastly/tests/parse_combo_module.rs index 56d8623a..54342ff9 100644 --- a/xlsynth-vastly/tests/parse_combo_module.rs +++ b/xlsynth-vastly/tests/parse_combo_module.rs @@ -331,6 +331,42 @@ endmodule assert!(compile_combo_module(bad_indexed_width).is_err()); } +#[test] +fn rejects_non_zero_based_decl_ranges() { + let bad_packed = r#" +module bad_packed_decl_v( + input wire [7:1] a, + output wire [6:0] out +); + assign out = a; +endmodule +"#; + let err = compile_combo_module(bad_packed).unwrap_err(); + assert!(matches!( + err, + xlsynth_vastly::Error::Parse(msg) + if msg.contains("packed declaration ranges must be zero-based") + )); + + let bad_unpacked = r#" +module bad_unpacked_decl_v( + input wire [1:0] in_data, + output wire [1:0] out +); + wire [1:0] lanes[0:1]; + assign lanes[0] = in_data; + assign lanes[1] = in_data; + assign out = lanes[0]; +endmodule +"#; + let err = compile_combo_module(bad_unpacked).unwrap_err(); + assert!(matches!( + err, + xlsynth_vastly::Error::Parse(msg) + if msg.contains("unpacked declaration ranges must be zero-based") + )); +} + #[test] fn signed_casts_are_self_determined_before_assignment_context() { let dut = r#" @@ -1208,7 +1244,7 @@ module fuzz_codegen_v( ); wire [2:0] tuple_11; wire [5:0] tuple_13; - wire [2:0] array_12[0:1]; + wire [2:0] array_12[1:0]; wire [38:0] tuple_15; wire [2:0] tuple_index_16; wire [10:0] zero_ext_17; @@ -1235,8 +1271,7 @@ endmodule ) .unwrap(); assert_eq!(out["out"].to_bit_string_msb_first(), "00000000101"); - assert_eq!(out["array_12[0]"].to_bit_string_msb_first(), "101"); - assert_eq!(out["array_12[1]"].to_bit_string_msb_first(), "101"); + assert_eq!(out["array_12"].to_bit_string_msb_first(), "101101"); } #[test] @@ -1284,9 +1319,9 @@ module fuzz_codegen_v( input wire [1:0] p0, output wire [1:0] out ); - wire [1:0] array_10[0:1]; - wire [1:0] array_11[0:1][0:1]; - wire [1:0] sel_12[0:1][0:1]; + wire [1:0] array_10[1:0]; + wire [1:0] array_11[1:0][1:0]; + wire [1:0] sel_12[1:0][1:0]; assign array_10[0] = p0; assign array_10[1] = ~p0; assign array_11[0][0] = array_10[0]; @@ -1313,8 +1348,8 @@ endmodule ) .unwrap(); assert_eq!(out["out"].to_bit_string_msb_first(), "10"); - assert_eq!(out["array_11[0][1]"].to_bit_string_msb_first(), "10"); - assert_eq!(out["sel_12[1][0]"].to_bit_string_msb_first(), "10"); + assert_eq!(out["array_11"].to_bit_string_msb_first(), "01101001"); + assert_eq!(out["sel_12"].to_bit_string_msb_first(), "01101001"); } #[test] @@ -1353,7 +1388,7 @@ endmodule ) .unwrap(); assert_eq!(out_nonzero["out"].to_bit_string_msb_first(), "01"); - assert_eq!(out_nonzero["sel_13[1][0]"].to_bit_string_msb_first(), "01"); + assert_eq!(out_nonzero["sel_13"].to_bit_string_msb_first(), "10010110"); let out_zero = eval_combo( &m, @@ -1364,7 +1399,7 @@ endmodule ) .unwrap(); assert_eq!(out_zero["out"].to_bit_string_msb_first(), "00"); - assert_eq!(out_zero["array_11[1][0]"].to_bit_string_msb_first(), "00"); + assert_eq!(out_zero["array_11"].to_bit_string_msb_first(), "11001100"); } #[test]