diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index b56391538..9929dc4c5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -5,7 +5,11 @@ //! Celox runtime IR. use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; -use std::ops::{Deref, DerefMut}; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + sync::Arc, +}; use sv_parser::{Locate, RefNode, SyntaxTree, unwrap_node}; @@ -149,7 +153,20 @@ impl Module { ) -> Result { let node = node.into(); let name = module_name_from_node(node.clone(), syntax_tree)?; - let mut parameters = parameters_from_module_node(node.clone(), syntax_tree)?; + let mut type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; + let empty_parameter_overrides = HashMap::default(); + let applicable_parameter_overrides = if name == override_module_name { + parameter_overrides + } else { + &empty_parameter_overrides + }; + let mut parameters = parameters_from_module_node( + node.clone(), + syntax_tree, + &type_aliases, + &HashMap::default(), + applicable_parameter_overrides, + )?; let mut parameter_names = HashSet::default(); if let Some(parameter) = parameters .iter() @@ -163,10 +180,113 @@ impl Module { if name == override_module_name { apply_parameter_overrides(&mut parameters, parameter_overrides)?; } - let const_env = const_env_from_parameters(¶meters); - let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - reject_silently_ignored_constructs(node.clone(), syntax_tree, &const_env, &type_aliases)?; - let ports = ports_from_module_node(node.clone(), syntax_tree)?; + let mut const_env = const_env_from_parameters(¶meters); + type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &const_env)?; + let enum_constants = enum_member_constants_from_module_node( + node.clone(), + syntax_tree, + &const_env, + &type_aliases, + applicable_parameter_overrides, + )?; + for (name, value) in &enum_constants.numbers { + const_env.entry(name.clone()).or_insert(*value); + const_env.insert(enum_marker(name), *value); + if let Some(r#type) = enum_constants.types.get(name) { + insert_parameter_type_markers(&mut const_env, name, *r#type); + } + } + // Constant casts are evaluated while parameter syntax is lowered. + // Repeat that lowering after enum constants become available so a + // cast operand such as `byte_t'(ENUM_MEMBER)` is not permanently + // discarded during the initial pass. Materialize only enum-member + // references so exported parameter expressions retain dependencies + // on other parameters for later specialization. + parameters = parameters_from_module_node( + node.clone(), + syntax_tree, + &type_aliases, + &const_env, + applicable_parameter_overrides, + )?; + for parameter in &mut parameters { + parameter.value = parameter.value.take().map(|value| { + substitute_typed_parameter_literals( + value, + &enum_constants.numbers, + &enum_constants.types, + ) + }); + } + if name == override_module_name { + apply_parameter_overrides(&mut parameters, parameter_overrides)?; + } + extend_const_env_with_parameters(&mut const_env, ¶meters); + type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &const_env)?; + + match reject_silently_ignored_constructs( + node.clone(), + syntax_tree, + &const_env, + &type_aliases, + ) { + Ok(()) => {} + // A parameter initializer may inspect a port or signal type + // through `$bits`/`$size`. Collect declarations only when that + // missing type metadata is the remaining validation failure, so + // unsupported constructs keep their original diagnostics. + Err(AnalyzerError::Unsupported(construct)) + if construct == "constant cast expression" => + { + let preliminary_ports = + ports_from_module_node(node.clone(), syntax_tree, &const_env, &type_aliases)?; + let preliminary_signals = + signals_from_module_node(node.clone(), syntax_tree, &const_env, &type_aliases)?; + extend_const_env_with_variable_types( + &mut const_env, + preliminary_ports + .iter() + .map(|port| (port.name(), port.r#type())) + .chain( + preliminary_signals + .iter() + .map(|signal| (signal.name(), signal.r#type())), + ), + ); + parameters = parameters_from_module_node( + node.clone(), + syntax_tree, + &type_aliases, + &const_env, + applicable_parameter_overrides, + )?; + for parameter in &mut parameters { + parameter.value = parameter.value.take().map(|value| { + substitute_typed_parameter_literals( + value, + &enum_constants.numbers, + &enum_constants.types, + ) + }); + } + if name == override_module_name { + apply_parameter_overrides(&mut parameters, parameter_overrides)?; + } + extend_const_env_with_parameters(&mut const_env, ¶meters); + type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &const_env)?; + reject_silently_ignored_constructs( + node.clone(), + syntax_tree, + &const_env, + &type_aliases, + )?; + } + Err(error) => return Err(error), + } + let ports = ports_from_module_node(node.clone(), syntax_tree, &const_env, &type_aliases)?; let mut port_names = HashSet::default(); if let Some(port) = ports.iter().find(|port| !port_names.insert(port.name())) { return Err(AnalyzerError::DuplicatePort { @@ -180,7 +300,8 @@ impl Module { { return Err(AnalyzerError::Unsupported("ref port direction".to_string())); } - let signals = signals_from_module_node(node.clone(), syntax_tree, &const_env)?; + let signals = + signals_from_module_node(node.clone(), syntax_tree, &const_env, &type_aliases)?; for r#type in ports .iter() .map(Port::r#type) @@ -199,7 +320,7 @@ impl Module { parameter.name() ))); } - let packed_dimensions = + let mut packed_dimensions = packed_dimensions_from_ports_and_signals(&ports, &signals, &const_env, &type_aliases); let mut instances = instances_from_module_node(node.clone(), syntax_tree, &const_env, &packed_dimensions)?; @@ -220,7 +341,12 @@ impl Module { }); } reject_unsupported_multidimensional_packed_bounds(&ports, &signals, &const_env)?; - let parameter_values = parameter_value_env(¶meters, &const_env); + let mut parameter_values = parameter_value_env(¶meters, &const_env); + for (name, value) in &enum_constants.exprs { + parameter_values + .entry(name.clone()) + .or_insert_with(|| value.clone()); + } let mut expression_signedness = ports .iter() .map(|port| (port.name().to_string(), port.r#type().is_signed())) @@ -237,10 +363,30 @@ impl Module { ); let functions = functions_from_module_node(node.clone(), syntax_tree, &const_env, &packed_dimensions)?; + packed_dimensions + .function_return_types + .extend(functions.iter().map(|(name, function)| { + ( + name.clone(), + FunctionReturnMetadata { + width: function.return_width, + first_packed_dimension_width: function.return_first_packed_dimension_width, + signed: function.return_signed, + is_2state: function.return_is_2state, + }, + ) + })); + packed_dimensions.functions = Arc::new(functions.clone()); + packed_dimensions.expression_signedness = Arc::new(expression_signedness.clone()); for instance in &mut instances { for connection in &mut instance.port_connections { connection.actual_expr = connection.actual_expr.take().map(|expr| { - expand_expr_calls(expr, &functions, &expression_signedness, 0, true) + let expr = expand_expr_calls(expr, &functions, &expression_signedness, 0, true); + substitute_expr_constants_with_parameter_literals( + expr, + &const_env, + &enum_constants.exprs, + ) }); } } @@ -249,6 +395,9 @@ impl Module { syntax_tree, &const_env, &packed_dimensions, + &functions, + &expression_signedness, + ¶meter_values, )? .into_iter() .map(|process| expand_process_calls(process, &functions, &expression_signedness)) @@ -401,6 +550,8 @@ fn reject_unsupported_multidimensional_packed_bounds( } } +const MAX_STATIC_PROCEDURAL_LOOP_EXPANSION: usize = 10_000; + fn static_for_loop_iterations( loop_statement: &sv_parser::LoopStatement, syntax_tree: &SyntaxTree, @@ -409,10 +560,12 @@ fn static_for_loop_iterations( let sv_parser::LoopStatement::For(loop_statement) = loop_statement else { return None; }; - let (initialization, _, condition, _, step) = &loop_statement.nodes.1.nodes.1; - let (name, initial_value) = for_loop_initialization(initialization.as_ref()?, syntax_tree) - .and_then(|(name, value)| Some((name, eval_ast_const_expr(&value, const_env)?)))?; - let initial_value = coerce_for_loop_index_value(initial_value)?; + let (_, _, condition, _, step) = &loop_statement.nodes.1.nodes.1; + let (name, initial_value) = + static_for_loop_initial_value(loop_statement, syntax_tree, const_env)?; + if for_loop_body_writes_index(loop_statement, &name, syntax_tree) { + return None; + } let condition = const_expr_from_expr(condition.as_ref()?, syntax_tree)?; let steps = step.as_ref()?.nodes.0.contents(); let [step] = steps.as_slice() else { @@ -421,7 +574,7 @@ fn static_for_loop_iterations( let mut values = Vec::new(); let mut value = initial_value; - for _ in 0..10_000 { + for _ in 0..MAX_STATIC_PROCEDURAL_LOOP_EXPANSION { let mut loop_env = const_env.clone(); loop_env.insert(name.clone(), value); insert_parameter_type_markers( @@ -454,6 +607,40 @@ fn static_for_loop_iterations( (eval_ast_const_expr(&condition, &loop_env) == Some(0)).then_some((name, values)) } +fn for_loop_body_writes_index( + loop_statement: &sv_parser::LoopStatementFor, + name: &str, + syntax_tree: &SyntaxTree, +) -> bool { + RefNode::StatementOrNull(&loop_statement.nodes.2) + .into_iter() + .any(|node| { + let lvalue = match node { + RefNode::BlockingAssignment(assignment) => match assignment { + sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.0, + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + &assignment.nodes.0 + } + _ => return false, + }, + RefNode::NonblockingAssignment(assignment) => &assignment.nodes.0, + _ => return false, + }; + for_loop_variable_lvalue_name(lvalue, syntax_tree).as_deref() == Some(name) + }) +} + +fn static_for_loop_initial_value( + loop_statement: &sv_parser::LoopStatementFor, + syntax_tree: &SyntaxTree, + const_env: &HashMap, +) -> Option<(String, i128)> { + let initialization = loop_statement.nodes.1.nodes.1.0.as_ref()?; + let (name, value) = for_loop_initialization(initialization, syntax_tree)?; + let value = eval_ast_const_expr(&value, const_env)?; + Some((name, coerce_for_loop_index_value(value)?)) +} + fn coerce_for_loop_index_value(value: i128) -> Option { const MODULUS: i128 = 1i128 << 32; const SIGN_BIT: i128 = 1i128 << 31; @@ -469,9 +656,15 @@ fn validate_static_for_loops_in_statement_or_null( statement: &sv_parser::StatementOrNull, syntax_tree: &SyntaxTree, const_env: &HashMap, + remaining_expansion: &mut usize, ) -> Result<(), AnalyzerError> { if let sv_parser::StatementOrNull::Statement(statement) = statement { - validate_static_for_loops_in_statement(statement, syntax_tree, const_env)?; + validate_static_for_loops_in_statement_with_budget( + statement, + syntax_tree, + const_env, + remaining_expansion, + )?; } Ok(()) } @@ -480,6 +673,21 @@ fn validate_static_for_loops_in_statement( statement: &sv_parser::Statement, syntax_tree: &SyntaxTree, const_env: &HashMap, +) -> Result<(), AnalyzerError> { + let mut remaining_expansion = MAX_STATIC_PROCEDURAL_LOOP_EXPANSION; + validate_static_for_loops_in_statement_with_budget( + statement, + syntax_tree, + const_env, + &mut remaining_expansion, + ) +} + +fn validate_static_for_loops_in_statement_with_budget( + statement: &sv_parser::Statement, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + remaining_expansion: &mut usize, ) -> Result<(), AnalyzerError> { match &statement.nodes.2 { sv_parser::StatementItem::ProceduralTimingControlStatement(timing) => { @@ -487,11 +695,17 @@ fn validate_static_for_loops_in_statement( &timing.nodes.1, syntax_tree, const_env, + remaining_expansion, )?; } sv_parser::StatementItem::SeqBlock(block) => { for statement in &block.nodes.3 { - validate_static_for_loops_in_statement_or_null(statement, syntax_tree, const_env)?; + validate_static_for_loops_in_statement_or_null( + statement, + syntax_tree, + const_env, + remaining_expansion, + )?; } } sv_parser::StatementItem::ConditionalStatement(conditional) => { @@ -499,12 +713,23 @@ fn validate_static_for_loops_in_statement( &conditional.nodes.3, syntax_tree, const_env, + remaining_expansion, )?; for (_, _, _, branch) in &conditional.nodes.4 { - validate_static_for_loops_in_statement_or_null(branch, syntax_tree, const_env)?; + validate_static_for_loops_in_statement_or_null( + branch, + syntax_tree, + const_env, + remaining_expansion, + )?; } if let Some((_, branch)) = &conditional.nodes.5 { - validate_static_for_loops_in_statement_or_null(branch, syntax_tree, const_env)?; + validate_static_for_loops_in_statement_or_null( + branch, + syntax_tree, + const_env, + remaining_expansion, + )?; } } sv_parser::StatementItem::CaseStatement(case) => { @@ -516,7 +741,12 @@ fn validate_static_for_loops_in_statement( sv_parser::CaseItem::NonDefault(item) => &item.nodes.2, sv_parser::CaseItem::Default(item) => &item.nodes.2, }; - validate_static_for_loops_in_statement_or_null(statement, syntax_tree, const_env)?; + validate_static_for_loops_in_statement_or_null( + statement, + syntax_tree, + const_env, + remaining_expansion, + )?; } } sv_parser::StatementItem::LoopStatement(loop_statement) => { @@ -524,6 +754,14 @@ fn validate_static_for_loops_in_statement( .ok_or_else(|| { AnalyzerError::Unsupported("procedural loop inside always_ff".to_string()) })?; + *remaining_expansion = + remaining_expansion + .checked_sub(values.len()) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "procedural loop unroll limit exceeded".to_string(), + ) + })?; let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { unreachable!(); }; @@ -542,6 +780,7 @@ fn validate_static_for_loops_in_statement( &loop_statement.nodes.2, syntax_tree, &loop_env, + remaining_expansion, )?; } } @@ -605,12 +844,26 @@ fn cast_target_type( return None; }; let name = identifier_text(RefNode::TypeIdentifier(&identifier.nodes.1), syntax_tree)?; - expr_type_from_type(type_aliases.get(&name)?, const_env) + if let Some(r#type) = type_aliases.get(&name) { + expr_type_from_type(r#type, const_env) + } else { + Some(ExprType { + width: usize::try_from(*const_env.get(&name)?).ok()?.max(1), + signed: false, + }) + } } sv_parser::CastingType::ConstantPrimary(primary) => { + if let Some(r#type) = + size_system_function_expr_type(primary, syntax_tree, const_env, type_aliases) + { + return Some(r#type); + } let target = const_expr_from_ref_node(RefNode::ConstantPrimary(primary), syntax_tree)?; if let ConstExpr::Ident(name) = &target { - return expr_type_from_type(type_aliases.get(name)?, const_env); + if let Some(r#type) = type_aliases.get(name) { + return expr_type_from_type(r#type, const_env); + } } let width = eval_ast_const_expr(&target, const_env)?; Some(ExprType { @@ -622,375 +875,962 @@ fn cast_target_type( } } -fn expr_type_from_type(r#type: &Type, const_env: &HashMap) -> Option { - if !r#type.unpacked_ranges().is_empty() { - return None; - } - let width = r#type - .packed_ranges() - .iter() - .try_fold(1usize, |width, range| { - let left = eval_ast_const_expr(range.left(), const_env)?; - let right = eval_ast_const_expr(range.right(), const_env)?; - width.checked_mul(usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?) - })?; - Some(ExprType { - width: width.max(1), - signed: r#type.is_signed(), - }) -} - -fn cast_is_supported( - cast: &sv_parser::Cast, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> bool { - cast_zero_type(cast, syntax_tree, const_env, type_aliases).is_some() -} - -fn constant_cast_is_supported( - cast: &sv_parser::ConstantCast, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> bool { - constant_cast_zero_type(cast, syntax_tree, const_env, type_aliases).is_some() -} - -fn cast_zero_type( - cast: &sv_parser::Cast, +fn size_system_function_expr_type( + primary: &sv_parser::ConstantPrimary, syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, ) -> Option { - let ConstExpr::Literal(literal) = const_expr_from_expr(&cast.nodes.2.nodes.1, syntax_tree)? - else { + let sv_parser::ConstantPrimary::ConstantFunctionCall(call) = primary else { return None; }; - let literal = typecheck::parse_integral_literal(&literal)?; - if literal.value != 0u8.into() || literal.mask != 0u8.into() { + let sv_parser::SubroutineCall::SystemTfCall(system_call) = &call.nodes.0.nodes.0 else { + return None; + }; + let (name, r#type) = match &**system_call { + sv_parser::SystemTfCall::ArgDataType(call) => { + let name = syntax_tree.get_str(&call.nodes.0.nodes.0)?; + let data_type = &call.nodes.1.nodes.1.0; + let r#type = match data_type { + sv_parser::DataType::Type(data_type) => { + let name = + identifier_text(RefNode::TypeIdentifier(&data_type.nodes.1), syntax_tree)?; + type_aliases.get(&name).cloned() + } + _ => type_from_ref_node_with_env( + RefNode::DataType(data_type), + syntax_tree, + const_env, + type_aliases, + ), + }?; + (name, r#type) + } + // sv-parser classifies an unqualified typedef argument as an + // expression because its grammar cannot know whether the identifier + // names a type. Resolve that ambiguity from the module alias table. + sv_parser::SystemTfCall::ArgExpression(call) => { + let name = syntax_tree.get_str(&call.nodes.0.nodes.0)?; + let arguments = call.nodes.1.nodes.1.0.contents(); + if arguments.len() != 1 { + return None; + } + let argument = arguments[0].as_ref()?; + if name != "$bits" && name != "$size" { + return None; + } + // A size-function argument only needs a statically known type; + // it need not itself be a constant expression. Lower the typed + // expression first so selects and other runtime-valued forms can + // still determine the cast width. + if let Some(r#type) = size_function_expression_type( + argument, + syntax_tree, + const_env, + type_aliases, + name == "$size", + ) { + return Some(r#type); + } + let argument = const_expr_from_expr(argument, syntax_tree)?; + if let ConstExpr::Ident(alias) = &argument + && let Some(r#type) = type_aliases.get(alias) + { + (name, r#type.clone()) + } else { + if let ConstExpr::Ident(identifier) = &argument + && let Some(width) = + variable_size_function_width(const_env, identifier, name == "$size") + { + return Some(ExprType { + width, + signed: variable_type_is_signed(const_env, identifier), + }); + } + let r#type = + infer_const_expr_type(&argument, ¶meter_types_from_const_env(const_env))?; + return Some(ExprType { + width: r#type.width.max(1), + signed: r#type.signed, + }); + } + } + sv_parser::SystemTfCall::ArgOptionl(_) => return None, + }; + // $bits covers every unpacked and packed dimension; $size covers only + // the outermost dimension, which is unpacked when one is present. + let first_dimension_only = name == "$size"; + if name != "$bits" && !first_dimension_only { return None; } - let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; - let signed = if matches!(cast.nodes.0, sv_parser::CastingType::ConstantPrimary(_)) { - literal.signed - } else { - target_type.signed + if !first_dimension_only { + let unpacked_width = r#type + .unpacked_ranges() + .iter() + .try_fold(1usize, |width, range| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + width.checked_mul(usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?) + })?; + let packed_width = r#type + .packed_ranges() + .iter() + .try_fold(1usize, |width, range| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + width.checked_mul(usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?) + })?; + return Some(ExprType { + width: unpacked_width.checked_mul(packed_width)?.max(1), + signed: r#type.is_signed(), + }); + } + let range = r#type + .unpacked_ranges() + .first() + .map(|range| (range.left(), range.right())) + .or_else(|| { + r#type + .packed_ranges() + .first() + .map(|range| (range.left(), range.right())) + }); + let Some((left, right)) = range else { + return Some(ExprType { + width: 1, + signed: r#type.is_signed(), + }); }; + let left = eval_ast_const_expr(left, const_env)?; + let right = eval_ast_const_expr(right, const_env)?; + let width = usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?; Some(ExprType { - signed, - ..target_type + width: width.max(1), + signed: r#type.is_signed(), }) } -fn constant_cast_zero_type( - cast: &sv_parser::ConstantCast, +fn size_function_expression_type( + argument: &sv_parser::Expression, syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, + first_dimension_only: bool, ) -> Option { - let ConstExpr::Literal(literal) = const_expr_from_ref_node( - RefNode::ConstantExpression(&cast.nodes.2.nodes.1), + let mut packed_dimensions = containing_packed_dimensions( + RefNode::Expression(argument), syntax_tree, - )? - else { - return None; - }; - let literal = typecheck::parse_integral_literal(&literal)?; - if literal.value != 0u8.into() || literal.mask != 0u8.into() { - return None; - } - let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; - let signed = if matches!(cast.nodes.0, sv_parser::CastingType::ConstantPrimary(_)) { - literal.signed + const_env, + type_aliases, + ) + .unwrap_or_else(|| PackedDimensions { + const_env: const_env.clone(), + type_aliases: type_aliases.clone(), + ..PackedDimensions::default() + }); + packed_dimensions.function_return_types = containing_function_return_types( + RefNode::Expression(argument), + syntax_tree, + const_env, + type_aliases, + ); + let expression = expr_from_expression_with_types(argument, syntax_tree, &packed_dimensions)?; + let width = if first_dimension_only { + selected_expression_first_dimension_width(argument, syntax_tree, &packed_dimensions) + .or_else(|| match &expression { + Expr::Ident(name) => variable_size_function_width(const_env, name, true), + Expr::Call { name, .. } => packed_dimensions + .function_return_types + .get(name) + .and_then(|metadata| metadata.first_packed_dimension_width), + _ => expr_static_width(&expression, &packed_dimensions), + }) } else { - target_type.signed - }; + expr_static_width(&expression, &packed_dimensions) + }?; + let mut identifier_signedness = parameter_types_from_const_env(const_env) + .into_iter() + .map(|(name, r#type)| (name, r#type.signed)) + .collect::>(); + const VARIABLE_SIGNED_PREFIX: &str = "__variable::signed::"; + identifier_signedness.extend(const_env.iter().filter_map(|(marker, signed)| { + marker + .strip_prefix(VARIABLE_SIGNED_PREFIX) + .map(|name| (name.to_string(), *signed != 0)) + })); + let signed = expr_signedness_with_return_types( + &expression, + &identifier_signedness, + &HashMap::default(), + &packed_dimensions.function_return_types, + )?; Some(ExprType { + width: width.max(1), signed, - ..target_type }) } -fn typed_zero_literal(r#type: ExprType) -> String { - format!( - "{}'{}d0", - r#type.width, - if r#type.signed { "s" } else { "" } - ) -} - -fn for_loop_variable_lvalue_name( - lvalue: &sv_parser::VariableLvalue, +fn selected_expression_first_dimension_width( + argument: &sv_parser::Expression, syntax_tree: &SyntaxTree, -) -> Option { - let sv_parser::VariableLvalue::Identifier(identifier) = lvalue else { + packed_dimensions: &PackedDimensions, +) -> Option { + let sv_parser::Expression::Primary(primary) = argument else { return None; }; - identifier_text( - RefNode::HierarchicalVariableIdentifier(&identifier.nodes.1), + if let sv_parser::Primary::MintypmaxExpression(grouped) = &**primary + && let sv_parser::MintypmaxExpression::Expression(argument) = &grouped.nodes.0.nodes.1 + { + return selected_expression_first_dimension_width(argument, syntax_tree, packed_dimensions); + } + let sv_parser::Primary::Hierarchical(hierarchical) = &**primary else { + return None; + }; + let name = identifier_text( + RefNode::HierarchicalIdentifier(&hierarchical.nodes.1), syntax_tree, - ) -} - -fn next_for_loop_value( - step: &sv_parser::ForStepAssignment, - name: &str, - value: i128, - syntax_tree: &SyntaxTree, + )?; + let dimensions = packed_dimensions.get(&name)?; + let select = &hierarchical.nodes.2; + if let Some(range) = &select.nodes.2 { + let sv_parser::PartSelectRange::ConstantRange(range) = &range.nodes.1 else { + return None; + }; + let bound = |expression| { + let expression = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(expression), + syntax_tree, + &packed_dimensions.const_env, + &packed_dimensions.type_aliases, + )?; + eval_ast_const_expr(&expression, &packed_dimensions.const_env) + }; + return usize::try_from(bound(&range.nodes.0)?.abs_diff(bound(&range.nodes.2)?)) + .ok()? + .checked_add(1); + } + // Each index removes one declared dimension. Inspect the syntax before + // flattening, which otherwise loses the remaining array shape. + let index_count = select.nodes.1.nodes.0.len(); + let width = dimensions + .unpacked + .iter() + .map(|dimension| &dimension.width) + .chain(dimensions.packed.iter().map(|dimension| &dimension.width)) + .nth(index_count); + match width { + Some(width) => { + usize::try_from(eval_ast_const_expr(width, &packed_dimensions.const_env)?).ok() + } + None if index_count == dimensions.unpacked.len() + dimensions.packed.len() => Some(1), + None => None, + } +} + +fn containing_packed_dimensions( + target: RefNode<'_>, + syntax_tree: &SyntaxTree, const_env: &HashMap, -) -> Option { - match step { - sv_parser::ForStepAssignment::OperatorAssignment(step) => { - if for_loop_variable_lvalue_name(&step.nodes.0, syntax_tree)? != name { - return None; - } - let operator = syntax_tree.get_str(&step.nodes.1.nodes.0)?.trim(); - let rhs = const_expr_from_expr(&step.nodes.2, syntax_tree)?; - if operator == "=" { - return eval_ast_const_expr(&rhs, const_env); - } - let op = match operator { - "+=" => BinaryOp::Add, - "-=" => BinaryOp::Sub, - "*=" => BinaryOp::Mul, - "/=" => BinaryOp::Div, - "%=" => BinaryOp::Mod, - _ => return None, - }; - let lhs = ConstExpr::Literal(format_typed_parameter_literal(value, 32, true)); - let expression = ConstExpr::Binary { - left: Box::new(lhs), - op, - right: Box::new(rhs), - }; - eval_ast_const_expr(&expression, const_env) + type_aliases: &HashMap, +) -> Option { + let (target_start, target_end) = ref_node_source_span(target)?; + for node in syntax_tree { + let module = match node { + RefNode::ModuleDeclarationAnsi(module) => RefNode::ModuleDeclarationAnsi(module), + RefNode::ModuleDeclarationNonansi(module) => RefNode::ModuleDeclarationNonansi(module), + _ => continue, + }; + let Some((module_start, module_end)) = ref_node_source_span(module.clone()) else { + continue; + }; + if target_start < module_start || target_end > module_end { + continue; } - sv_parser::ForStepAssignment::IncOrDecExpression(step) => { - let (lvalue, operator) = match &**step { - sv_parser::IncOrDecExpression::Prefix(step) => (&step.nodes.2, &step.nodes.0), - sv_parser::IncOrDecExpression::Suffix(step) => (&step.nodes.0, &step.nodes.2), - }; - if for_loop_variable_lvalue_name(lvalue, syntax_tree)? != name { - return None; - } - match syntax_tree.get_str(&operator.nodes.0)? { - "++" => value.checked_add(1), - "--" => value.checked_sub(1), - _ => None, - } + let module_span = (module_start, module_end); + if !ACTIVE_PACKED_DIMENSIONS.with(|active| active.borrow_mut().insert(module_span)) { + return None; } - sv_parser::ForStepAssignment::FunctionSubroutineCall(_) => None, + // A declaration range can query a size that requires this same + // module's metadata. Recursive discovery uses the caller's constant + // and function type environments instead of rebuilding declarations. + let _guard = ActivePackedDimensionsGuard { module_span }; + let ports = + ports_from_module_node(module.clone(), syntax_tree, const_env, type_aliases).ok()?; + let signals = + signals_from_module_node(module, syntax_tree, const_env, type_aliases).ok()?; + return Some(packed_dimensions_from_ports_and_signals( + &ports, + &signals, + const_env, + type_aliases, + )); } + None } -fn reject_silently_ignored_constructs( - node: RefNode<'_>, +fn containing_function_return_types( + target: RefNode<'_>, syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, -) -> Result<(), AnalyzerError> { - let inactive_nodes = inactive_conditional_generate_nodes(node.clone(), syntax_tree, const_env); - let has_leaking_conditional_generate_local = - conditional_generate_has_leaking_local(node.clone(), syntax_tree); - reject_duplicate_conditional_generate_locals(node.clone(), syntax_tree)?; - for child in node { - if inactive_nodes.iter().any(|inactive| inactive == &child) { +) -> HashMap { + let Some((target_start, target_end)) = ref_node_source_span(target) else { + return HashMap::default(); + }; + for node in syntax_tree { + let module = match node { + RefNode::ModuleDeclarationAnsi(module) => RefNode::ModuleDeclarationAnsi(module), + RefNode::ModuleDeclarationNonansi(module) => RefNode::ModuleDeclarationNonansi(module), + _ => continue, + }; + let Some((module_start, module_end)) = ref_node_source_span(module.clone()) else { + continue; + }; + if target_start < module_start || target_end > module_end { continue; } - match child { - RefNode::Cast(cast) - if !cast_is_supported(cast, syntax_tree, const_env, type_aliases) => - { - return Err(AnalyzerError::Unsupported("cast expression".to_string())); - } - RefNode::ConstantCast(cast) - if !constant_cast_is_supported(cast, syntax_tree, const_env, type_aliases) => - { - return Err(AnalyzerError::Unsupported("constant cast expression".to_string())); - } - RefNode::AnsiPortDeclarationNet(port) if port.nodes.3.is_some() => { - return Err(AnalyzerError::Unsupported( - "ANSI port default value".to_string(), - )); - } - RefNode::AnsiPortDeclarationVariable(port) if port.nodes.3.is_some() => { - return Err(AnalyzerError::Unsupported( - "ANSI port default value".to_string(), - )); - } - RefNode::AnsiPortDeclarationVariable(port) - if RefNode::AnsiPortDeclarationVariable(port) - .into_iter() - .any(|node| matches!(node, RefNode::DataTypeEnum(_))) => - { - return Err(AnalyzerError::Unsupported("enum port".to_string())); - } - RefNode::AnsiPortDeclarationNet(port) - if RefNode::AnsiPortDeclarationNet(port) - .into_iter() - .any(|node| matches!(node, RefNode::DataTypeEnum(_))) => - { - return Err(AnalyzerError::Unsupported("enum port".to_string())); - } - RefNode::AlwaysConstruct(always) => { - if matches!( - always.nodes.0, - sv_parser::AlwaysKeyword::Always(_) | sv_parser::AlwaysKeyword::AlwaysLatch(_) + let module_span = (module_start, module_end); + if let Some(active) = ACTIVE_FUNCTION_RETURN_METADATA + .with(|metadata| metadata.borrow().get(&module_span).cloned()) + { + return active; + } + ACTIVE_FUNCTION_RETURN_METADATA.with(|metadata| { + metadata + .borrow_mut() + .insert(module_span, HashMap::default()); + }); + let _guard = ActiveFunctionReturnMetadataGuard { module_span }; + let declarations = module + .into_iter() + .filter_map(|child| { + let RefNode::FunctionDeclaration(declaration) = child else { + return None; + }; + Some(declaration) + }) + .collect::>(); + let mut result = HashMap::default(); + // A return range may depend on a function declared later in the + // module. Revisit declarations after publishing each partial pass; + // recursive discovery reads that partial map instead of recursing. + for _ in 0..=declarations.len() { + for declaration in &declarations { + if let Some((name, metadata)) = function_declaration_return_metadata( + declaration, + syntax_tree, + const_env, + type_aliases, ) { - return Err(AnalyzerError::Unsupported( - "always and always_latch processes".to_string(), - )); - } - let body = RefNode::Statement(&always.nodes.1); - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) - && body.clone().into_iter().any(|node| { - matches!( - node, - RefNode::ConditionalStatement(_) | RefNode::CaseStatement(_) - ) - }) - { - return Err(AnalyzerError::Unsupported( - "control flow inside always_comb".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) - && body - .clone() - .into_iter() - .any(|node| matches!(node, RefNode::BlockingAssignment(_))) - { - return Err(AnalyzerError::Unsupported( - "blocking assignment inside always_ff".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) { - validate_static_for_loops_in_statement(&always.nodes.1, syntax_tree, const_env)?; - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) - && body.clone().into_iter().any(|node| { - matches!( - node, - RefNode::CaseStatement(case) - if !matches!( - case, - sv_parser::CaseStatement::Normal(case) - if matches!(case.nodes.1, sv_parser::CaseKeyword::Case(_)) - ) - ) - }) - { - return Err(AnalyzerError::Unsupported( - "casez, casex, or pattern case inside always_ff".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) - && body - .clone() - .into_iter() - .any(|node| matches!(node, RefNode::LoopStatement(_))) - { - return Err(AnalyzerError::Unsupported( - "procedural loop inside always_comb".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) - && body - .clone() - .into_iter() - .any(|node| matches!(node, RefNode::NonblockingAssignment(_))) - { - return Err(AnalyzerError::Unsupported( - "nonblocking assignment inside always_comb".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) - && body.clone().into_iter().any(|node| { - matches!( - node, - RefNode::EventExpressionExpression(event) - if event.nodes.2.is_some() - ) - }) - { - return Err(AnalyzerError::Unsupported( - "iff-qualified always_ff event".to_string(), - )); - } - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) - && body - .clone() - .into_iter() - .any(|node| matches!(node, RefNode::VariableLvalueLvalue(_))) - { - return Err(AnalyzerError::Unsupported( - "concatenated always_ff assignment target".to_string(), - )); - } - if body - .into_iter() - .any(|node| matches!(node, RefNode::DataDeclaration(_))) - { - return Err(AnalyzerError::Unsupported( - "procedural local data declaration".to_string(), - )); - } - } - RefNode::NetDeclAssignment(assignment) if assignment.nodes.2.is_some() => { - return Err(AnalyzerError::Unsupported( - "net declaration assignment".to_string(), - )); - } - RefNode::LoopGenerateConstruct(generate) => { - if RefNode::LoopGenerateConstruct(generate) - .into_iter() - .any(|node| matches!(node, RefNode::ModuleInstantiation(_))) - { - return Err(AnalyzerError::Unsupported( - "module instantiation inside loop-generate".to_string(), - )); - } - if generate_block_has_data_declaration(&generate.nodes.2) { - return Err(AnalyzerError::Unsupported( - "local data declaration inside loop-generate".to_string(), - )); - } - if RefNode::LoopGenerateConstruct(generate) - .into_iter() - .any(|node| { - matches!( - node, - RefNode::AlwaysConstruct(always) - if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) - ) - }) - { - return Err(AnalyzerError::Unsupported( - "always_ff inside loop-generate".to_string(), - )); + result.insert(name, metadata); } } - RefNode::InitialConstruct(_) => { - return Err(AnalyzerError::Unsupported("initial construct".to_string())); - } - RefNode::FinalConstruct(_) => { - return Err(AnalyzerError::Unsupported("final construct".to_string())); + ACTIVE_FUNCTION_RETURN_METADATA.with(|active| { + active.borrow_mut().insert(module_span, result.clone()); + }); + } + return result; + } + HashMap::default() +} + +thread_local! { + static ACTIVE_PACKED_DIMENSIONS: RefCell> = + RefCell::new(HashSet::default()); + static ACTIVE_FUNCTION_RETURN_METADATA: + RefCell>> = + RefCell::new(HashMap::default()); +} + +struct ActivePackedDimensionsGuard { + module_span: (usize, usize), +} + +impl Drop for ActivePackedDimensionsGuard { + fn drop(&mut self) { + ACTIVE_PACKED_DIMENSIONS.with(|active| { + active.borrow_mut().remove(&self.module_span); + }); + } +} + +struct ActiveFunctionReturnMetadataGuard { + module_span: (usize, usize), +} + +impl Drop for ActiveFunctionReturnMetadataGuard { + fn drop(&mut self) { + ACTIVE_FUNCTION_RETURN_METADATA.with(|metadata| { + metadata.borrow_mut().remove(&self.module_span); + }); + } +} + +fn ref_node_source_span(node: RefNode<'_>) -> Option<(usize, usize)> { + let mut start = None; + let mut end = None; + for child in node { + let RefNode::Locate(locate) = child else { + continue; + }; + start.get_or_insert(locate.offset); + end = Some(locate.offset.checked_add(locate.len)?); + } + start.zip(end) +} + +fn function_declaration_return_metadata( + declaration: &sv_parser::FunctionDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option<(String, FunctionReturnMetadata)> { + let (return_node, identifier) = match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => (&body.nodes.0, &body.nodes.2), + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => (&body.nodes.0, &body.nodes.2), + }; + let name = identifier_text(RefNode::FunctionIdentifier(identifier), syntax_tree)?; + let return_type = function_return_type(return_node, syntax_tree, const_env, type_aliases); + Some(( + name, + FunctionReturnMetadata { + width: return_type.map(|r#type| r#type.width), + first_packed_dimension_width: function_return_first_packed_dimension_width( + return_node, + syntax_tree, + const_env, + type_aliases, + return_type, + ), + signed: return_type.is_some_and(|r#type| r#type.signed), + is_2state: function_return_is_2state(return_node, syntax_tree, type_aliases), + }, + )) +} + +fn expr_type_from_type(r#type: &Type, const_env: &HashMap) -> Option { + if !r#type.unpacked_ranges().is_empty() { + return None; + } + let width = r#type + .packed_ranges() + .iter() + .try_fold(1usize, |width, range| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + width.checked_mul(usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?) + })?; + Some(ExprType { + width: width.max(1), + signed: r#type.is_signed(), + }) +} + +fn cast_is_supported( + cast: &sv_parser::Cast, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> bool { + cast_zero_type(cast, syntax_tree, const_env, type_aliases).is_some() +} + +fn constant_cast_is_supported( + cast: &sv_parser::ConstantCast, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> bool { + constant_cast_const_expr(cast, syntax_tree, const_env, type_aliases).is_some() +} + +fn cast_zero_type( + cast: &sv_parser::Cast, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + let ConstExpr::Literal(literal) = const_expr_from_expr(&cast.nodes.2.nodes.1, syntax_tree)? + else { + return None; + }; + let literal = typecheck::parse_integral_literal(&literal)?; + if literal.value != 0u8.into() || literal.mask != 0u8.into() { + return None; + } + let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; + let signed = + if casting_type_is_numeric_size(&cast.nodes.0, syntax_tree, const_env, type_aliases) { + literal.signed + } else { + target_type.signed + }; + Some(ExprType { + signed, + ..target_type + }) +} + +fn constant_cast_const_expr( + cast: &sv_parser::ConstantCast, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + let operand = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&cast.nodes.2.nodes.1), + syntax_tree, + const_env, + type_aliases, + )?; + let parameter_types = parameter_types_from_const_env(const_env); + let operand_type = infer_const_expr_type(&operand, ¶meter_types)?; + let literal = if let ConstExpr::Literal(literal) = &operand { + typecheck::parse_integral_literal(literal)? + } else { + let ir_operand: crate::ir::ConstExpr = operand.clone().into(); + let ir_parameter_types = parameter_types + .iter() + .map(|(name, r#type)| (name.clone(), (r#type.width, r#type.signed))) + .collect(); + typecheck::eval_const_integral_literal_with_types( + &ir_operand, + const_env, + &ir_parameter_types, + ) + .or_else(|| { + let operand_value = eval_ast_const_expr(&operand, const_env)?; + let operand_literal = format_typed_parameter_literal( + operand_value, + operand_type.width, + operand_type.signed, + ); + typecheck::parse_integral_literal(&operand_literal) + })? + }; + let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; + // A numeric size cast keeps the source expression's signedness; a type + // cast takes the target type's signedness. + let signed = + if casting_type_is_numeric_size(&cast.nodes.0, syntax_tree, const_env, type_aliases) { + literal.signed + } else { + target_type.signed + }; + let resized = match &operand { + ConstExpr::Literal(value) => { + resize_unbased_fill_literal_for_cast(value, target_type.width, signed).unwrap_or_else( + || resize_integral_literal_for_cast(literal, target_type.width, signed), + ) + } + _ => resize_integral_literal_for_cast(literal, target_type.width, signed), + }; + let resized = if cast_target_is_two_state(&cast.nodes.0, syntax_tree, const_env, type_aliases) { + two_state_integral_literal(&resized, target_type.width, signed)? + } else { + resized + }; + Some(ConstExpr::Literal(resized)) +} + +fn cast_target_is_two_state( + casting_type: &sv_parser::CastingType, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> bool { + if type_from_ref_node_with_env( + RefNode::CastingType(casting_type), + syntax_tree, + const_env, + type_aliases, + ) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit) + { + return true; + } + match casting_type { + sv_parser::CastingType::SimpleType(simple_type) => { + let sv_parser::SimpleType::PsTypeIdentifier(identifier) = simple_type.as_ref() else { + return false; + }; + identifier_text(RefNode::TypeIdentifier(&identifier.nodes.1), syntax_tree) + .and_then(|name| type_aliases.get(&name)) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit) + } + sv_parser::CastingType::ConstantPrimary(primary) => { + let Some(ConstExpr::Ident(name)) = + const_expr_from_ref_node(RefNode::ConstantPrimary(primary), syntax_tree) + else { + return false; + }; + type_aliases + .get(&name) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit) + } + _ => false, + } +} + +fn two_state_integral_literal(value: &str, width: usize, signed: bool) -> Option { + let mut literal = typecheck::parse_integral_literal(value)?; + let keep = (num_bigint::BigUint::from(1usize) << width) - num_bigint::BigUint::from(1usize); + literal.value &= &keep ^ &literal.mask; + literal.mask = num_bigint::BigUint::default(); + Some(resize_integral_literal_for_cast(literal, width, signed)) +} + +fn casting_type_is_numeric_size( + casting_type: &sv_parser::CastingType, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> bool { + match casting_type { + sv_parser::CastingType::ConstantPrimary(primary) => { + let Some(ConstExpr::Ident(name)) = + const_expr_from_ref_node(RefNode::ConstantPrimary(primary), syntax_tree) + else { + return true; + }; + !type_aliases.contains_key(&name) + } + sv_parser::CastingType::SimpleType(simple_type) => { + let sv_parser::SimpleType::PsTypeIdentifier(identifier) = simple_type.as_ref() else { + return false; + }; + let Some(name) = + identifier_text(RefNode::TypeIdentifier(&identifier.nodes.1), syntax_tree) + else { + return false; + }; + !type_aliases.contains_key(&name) && const_env.contains_key(&name) + } + _ => false, + } +} + +fn resize_unbased_fill_literal_for_cast(value: &str, width: usize, signed: bool) -> Option { + let normalized = value.trim().to_ascii_lowercase(); + let mut chars = normalized.chars(); + (chars.next()? == '\'' && chars.clone().count() == 1).then_some(())?; + let fill = chars.next()?; + matches!(fill, '0' | '1' | 'x' | 'z' | '?').then_some(())?; + let signing = if signed { "s" } else { "" }; + Some(format!( + "{width}'{signing}b{}", + fill.to_string().repeat(width) + )) +} + +fn resize_integral_literal_for_cast( + literal: typecheck::IntegralLiteral, + width: usize, + signed: bool, +) -> String { + let mut value = literal.value; + let mut mask = literal.mask; + if literal.signed && literal.width > 0 && literal.width < width { + let extension = ((num_bigint::BigUint::from(1usize) << (width - literal.width)) + - num_bigint::BigUint::from(1usize)) + << literal.width; + if value.bit((literal.width - 1) as u64) { + value |= &extension; + } + if mask.bit((literal.width - 1) as u64) { + mask |= extension; + } + } + let keep = (num_bigint::BigUint::from(1usize) << width) - num_bigint::BigUint::from(1usize); + value &= &keep; + mask &= keep; + let signing = if signed { "s" } else { "" }; + if mask == num_bigint::BigUint::default() { + return format!("{width}'{signing}d{value}"); + } + let bits = (0..width) + .rev() + .map(|bit| { + if mask.bit(bit as u64) { + if value.bit(bit as u64) { 'x' } else { 'z' } + } else if value.bit(bit as u64) { + '1' + } else { + '0' } - RefNode::ElaborationSystemTask(_) => { - return Err(AnalyzerError::Unsupported( - "elaboration system task".to_string(), - )); + }) + .collect::(); + format!("{width}'{signing}b{bits}") +} + +fn for_loop_variable_lvalue_name( + lvalue: &sv_parser::VariableLvalue, + syntax_tree: &SyntaxTree, +) -> Option { + let sv_parser::VariableLvalue::Identifier(identifier) = lvalue else { + return None; + }; + identifier_text( + RefNode::HierarchicalVariableIdentifier(&identifier.nodes.1), + syntax_tree, + ) +} + +fn next_for_loop_value( + step: &sv_parser::ForStepAssignment, + name: &str, + value: i128, + syntax_tree: &SyntaxTree, + const_env: &HashMap, +) -> Option { + match step { + sv_parser::ForStepAssignment::OperatorAssignment(step) => { + if for_loop_variable_lvalue_name(&step.nodes.0, syntax_tree)? != name { + return None; } - RefNode::BindDirective(_) => { - return Err(AnalyzerError::Unsupported("bind directive".to_string())); + let operator = syntax_tree.get_str(&step.nodes.1.nodes.0)?.trim(); + let rhs = const_expr_from_expr(&step.nodes.2, syntax_tree)?; + if operator == "=" { + return eval_ast_const_expr(&rhs, const_env); } - RefNode::SpecifyBlock(_) => { - return Err(AnalyzerError::Unsupported("specify block".to_string())); + let op = match operator { + "+=" => BinaryOp::Add, + "-=" => BinaryOp::Sub, + "*=" => BinaryOp::Mul, + "/=" => BinaryOp::Div, + "%=" => BinaryOp::Mod, + _ => return None, + }; + let lhs = ConstExpr::Literal(format_typed_parameter_literal(value, 32, true)); + let expression = ConstExpr::Binary { + left: Box::new(lhs), + op, + right: Box::new(rhs), + }; + eval_ast_const_expr(&expression, const_env) + } + sv_parser::ForStepAssignment::IncOrDecExpression(step) => { + let (lvalue, operator) = match &**step { + sv_parser::IncOrDecExpression::Prefix(step) => (&step.nodes.2, &step.nodes.0), + sv_parser::IncOrDecExpression::Suffix(step) => (&step.nodes.0, &step.nodes.2), + }; + if for_loop_variable_lvalue_name(lvalue, syntax_tree)? != name { + return None; } - RefNode::MintypmaxExpressionTernary(_) - | RefNode::ConstantMintypmaxExpressionTernary(_) => { + match syntax_tree.get_str(&operator.nodes.0)? { + "++" => value.checked_add(1), + "--" => value.checked_sub(1), + _ => None, + } + } + sv_parser::ForStepAssignment::FunctionSubroutineCall(_) => None, + } +} + +fn reject_silently_ignored_constructs( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Result<(), AnalyzerError> { + let inactive_nodes = + inactive_conditional_generate_nodes(node.clone(), syntax_tree, const_env, type_aliases); + let has_leaking_conditional_generate_local = + conditional_generate_has_leaking_local(node.clone(), syntax_tree); + reject_duplicate_conditional_generate_locals(node.clone(), syntax_tree)?; + for child in node { + if inactive_nodes.iter().any(|inactive| inactive == &child) { + continue; + } + match child { + RefNode::Cast(cast) + if !cast_is_supported(cast, syntax_tree, const_env, type_aliases) => + { + return Err(AnalyzerError::Unsupported("cast expression".to_string())); + } + RefNode::ConstantCast(cast) + if !constant_cast_is_supported(cast, syntax_tree, const_env, type_aliases) => + { + return Err(AnalyzerError::Unsupported("constant cast expression".to_string())); + } + RefNode::AnsiPortDeclarationNet(port) if port.nodes.3.is_some() => { + return Err(AnalyzerError::Unsupported( + "ANSI port default value".to_string(), + )); + } + RefNode::AnsiPortDeclarationVariable(port) if port.nodes.3.is_some() => { + return Err(AnalyzerError::Unsupported( + "ANSI port default value".to_string(), + )); + } + RefNode::AnsiPortDeclarationVariable(port) + if RefNode::AnsiPortDeclarationVariable(port) + .into_iter() + .any(|node| matches!(node, RefNode::DataTypeEnum(_))) => + { + return Err(AnalyzerError::Unsupported("enum port".to_string())); + } + RefNode::AnsiPortDeclarationNet(port) + if RefNode::AnsiPortDeclarationNet(port) + .into_iter() + .any(|node| matches!(node, RefNode::DataTypeEnum(_))) => + { + return Err(AnalyzerError::Unsupported("enum port".to_string())); + } + RefNode::AlwaysConstruct(always) => { + if matches!( + always.nodes.0, + sv_parser::AlwaysKeyword::Always(_) | sv_parser::AlwaysKeyword::AlwaysLatch(_) + ) { + return Err(AnalyzerError::Unsupported( + "always and always_latch processes".to_string(), + )); + } + let body = RefNode::Statement(&always.nodes.1); + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) + && body + .clone() + .into_iter() + .any(|node| matches!(node, RefNode::BlockingAssignment(_))) + { + return Err(AnalyzerError::Unsupported( + "blocking assignment inside always_ff".to_string(), + )); + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) { + validate_static_for_loops_in_statement(&always.nodes.1, syntax_tree, const_env)?; + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) { + validate_static_for_loops_in_statement(&always.nodes.1, syntax_tree, const_env) + .map_err(|error| match error { + AnalyzerError::Unsupported(construct) + if construct == "procedural loop inside always_ff" => + { + AnalyzerError::Unsupported( + "procedural loop inside always_comb".to_string(), + ) + } + error => error, + })?; + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) + && body.clone().into_iter().any(|node| { + matches!( + node, + RefNode::CaseStatement(case) + if !matches!( + case, + sv_parser::CaseStatement::Normal(case) + if matches!(case.nodes.1, sv_parser::CaseKeyword::Case(_)) + ) + ) + }) + { + return Err(AnalyzerError::Unsupported( + "casez, casex, or pattern case inside always_ff".to_string(), + )); + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) + && body + .clone() + .into_iter() + .any(|node| matches!(node, RefNode::NonblockingAssignment(_))) + { + return Err(AnalyzerError::Unsupported( + "nonblocking assignment inside always_comb".to_string(), + )); + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) + && body.clone().into_iter().any(|node| { + matches!( + node, + RefNode::EventExpressionExpression(event) + if event.nodes.2.is_some() + ) + }) + { + return Err(AnalyzerError::Unsupported( + "iff-qualified always_ff event".to_string(), + )); + } + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) + && body + .clone() + .into_iter() + .any(|node| matches!(node, RefNode::VariableLvalueLvalue(_))) + { + return Err(AnalyzerError::Unsupported( + "concatenated always_ff assignment target".to_string(), + )); + } + if body + .into_iter() + .any(|node| matches!(node, RefNode::DataDeclaration(_))) + { + let detail = if matches!( + always.nodes.0, + sv_parser::AlwaysKeyword::AlwaysComb(_) + ) { + "block-local declaration inside always_comb" + } else { + "procedural local data declaration" + }; + return Err(AnalyzerError::Unsupported(detail.to_string())); + } + } + RefNode::NetDeclAssignment(assignment) if assignment.nodes.2.is_some() => { + return Err(AnalyzerError::Unsupported( + "net declaration assignment".to_string(), + )); + } + RefNode::LoopGenerateConstruct(generate) => { + if RefNode::LoopGenerateConstruct(generate) + .into_iter() + .any(|node| matches!(node, RefNode::ModuleInstantiation(_))) + { + return Err(AnalyzerError::Unsupported( + "module instantiation inside loop-generate".to_string(), + )); + } + if generate_block_has_data_declaration(&generate.nodes.2) { + return Err(AnalyzerError::Unsupported( + "local data declaration inside loop-generate".to_string(), + )); + } + if RefNode::LoopGenerateConstruct(generate) + .into_iter() + .any(|node| { + matches!( + node, + RefNode::AlwaysConstruct(always) + if matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) + ) + }) + { + return Err(AnalyzerError::Unsupported( + "always_ff inside loop-generate".to_string(), + )); + } + } + RefNode::InitialConstruct(_) => { + return Err(AnalyzerError::Unsupported("initial construct".to_string())); + } + RefNode::FinalConstruct(_) => { + return Err(AnalyzerError::Unsupported("final construct".to_string())); + } + RefNode::ElaborationSystemTask(_) => { + return Err(AnalyzerError::Unsupported( + "elaboration system task".to_string(), + )); + } + RefNode::BindDirective(_) => { + return Err(AnalyzerError::Unsupported("bind directive".to_string())); + } + RefNode::SpecifyBlock(_) => { + return Err(AnalyzerError::Unsupported("specify block".to_string())); + } + RefNode::MintypmaxExpressionTernary(_) + | RefNode::ConstantMintypmaxExpressionTernary(_) => { return Err(AnalyzerError::Unsupported( "mintypmax expression".to_string(), )); @@ -1168,2654 +2008,5250 @@ fn reject_silently_ignored_constructs( "trireg charge storage".to_string(), )); } - RefNode::PackedDimensionRange(range) - if const_expr_from_ref_node( - RefNode::ConstantExpression(&range.nodes.0.nodes.1.nodes.0), + RefNode::PackedDimensionRange(range) + if const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&range.nodes.0.nodes.1.nodes.0), + syntax_tree, + const_env, + type_aliases, + ) + .is_none() + || const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&range.nodes.0.nodes.1.nodes.2), + syntax_tree, + const_env, + type_aliases, + ) + .is_none() => + { + return Err(AnalyzerError::Unsupported( + "unsupported packed range".to_string(), + )); + } + RefNode::PackageImportDeclaration(_) | RefNode::PackageScope(_) => { + return Err(AnalyzerError::Unsupported( + "package-dependent systemverilog module".to_string(), + )); + } + RefNode::ParamAssignment(parameter) + if RefNode::ParamAssignment(parameter).into_iter().any(|node| { + matches!( + node, + RefNode::ConstantExpression( + sv_parser::ConstantExpression::Unary(unary) + ) if syntax_tree + .get_str(&unary.nodes.0.nodes.0.nodes.0) + .is_some_and(|op| op == "~") + && matches!( + const_expr_from_ref_node( + RefNode::ConstantPrimary(&unary.nodes.2), + syntax_tree, + ), + Some(ConstExpr::Ident(_)) + ) + ) + }) => + { + return Err(AnalyzerError::Unsupported( + "width-dependent complement in parameter expression".to_string(), + )); + } + RefNode::ParamAssignment(parameter) + if RefNode::ParamAssignment(parameter).into_iter().any(|node| { + matches!( + node, + RefNode::UnaryOperator(operator) + if syntax_tree + .get_str(&operator.nodes.0) + .is_some_and(|op| matches!(op, "&" | "|" | "^" | "~&" | "~|" | "~^" | "^~")) + ) + }) => + { + return Err(AnalyzerError::Unsupported( + "reduction operator in parameter expression".to_string(), + )); + } + _ => {} + } + } + Ok(()) +} + +fn inactive_conditional_generate_nodes<'a>( + node: RefNode<'a>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Vec> { + let mut selections = Vec::new(); + for item in module_non_port_items(node) { + generate_selections_from_non_port_item( + item, + syntax_tree, + const_env, + type_aliases, + &mut selections, + ); + } + selections + .into_iter() + .filter(|(_, selected)| !selected) + .flat_map(|(block, _)| RefNode::GenerateBlock(block)) + .collect() +} + +fn record_generate_block_selection<'a>( + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, + block: &'a sv_parser::GenerateBlock, + selected: bool, +) { + if let Some((_, previously_selected)) = selections + .iter_mut() + .find(|(candidate, _)| *candidate == block) + { + *previously_selected |= selected; + } else { + selections.push((block, selected)); + } +} + +fn generate_selections_from_non_port_item<'a>( + item: &'a sv_parser::NonPortModuleItem, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + match item { + sv_parser::NonPortModuleItem::GenerateRegion(region) => { + for item in ®ion.nodes.1 { + generate_selections_from_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + } + sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { + generate_selections_from_module_or_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + _ => {} + } +} + +fn generate_selections_from_generate_item<'a>( + item: &'a sv_parser::GenerateItem, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { + generate_selections_from_module_or_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } +} + +fn generate_selections_from_module_or_generate_item<'a>( + item: &'a sv_parser::ModuleOrGenerateItem, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item else { + return; + }; + match &item.nodes.1 { + sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { + generate_selections_from_conditional_generate( + generate, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + sv_parser::ModuleCommonItem::LoopGenerateConstruct(generate) => { + generate_selections_from_loop_generate( + generate, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + _ => {} + } +} + +fn generate_selections_from_conditional_generate<'a>( + generate: &'a sv_parser::ConditionalGenerateConstruct, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { + return; + }; + let condition = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1), + syntax_tree, + const_env, + type_aliases, + ) + .and_then(|condition| eval_ast_const_expr(&condition, const_env)); + let then_selected = condition.map(|condition| condition != 0).unwrap_or(true); + record_generate_block_selection(selections, &generate.nodes.2, then_selected); + if then_selected { + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + if let Some((_, block)) = &generate.nodes.3 { + let else_selected = condition.map(|condition| condition == 0).unwrap_or(true); + record_generate_block_selection(selections, block, else_selected); + if else_selected { + generate_selections_from_generate_block( + block, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + } +} + +fn generate_selections_from_loop_generate<'a>( + generate: &'a sv_parser::LoopGenerateConstruct, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + let Some(name) = identifier_text( + RefNode::GenvarIdentifier(&generate.nodes.1.nodes.1.0.nodes.1), + syntax_tree, + ) else { + record_generate_block_selection(selections, &generate.nodes.2, true); + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + const_env, + type_aliases, + selections, + ); + return; + }; + let Some(init) = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1.0.nodes.3), + syntax_tree, + const_env, + type_aliases, + ) + .and_then(|init| eval_ast_const_expr(&init, const_env)) else { + record_generate_block_selection(selections, &generate.nodes.2, true); + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + const_env, + type_aliases, + selections, + ); + return; + }; + let Some(condition) = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1.2.nodes.0), + syntax_tree, + const_env, + type_aliases, + ) else { + record_generate_block_selection(selections, &generate.nodes.2, true); + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + const_env, + type_aliases, + selections, + ); + return; + }; + + let mut value = init; + let mut selected = false; + for _ in 0..10_000 { + let mut loop_env = const_env.clone(); + loop_env.insert(name.clone(), value); + let Some(condition_value) = eval_ast_const_expr(&condition, &loop_env) else { + record_generate_block_selection(selections, &generate.nodes.2, true); + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + &loop_env, + type_aliases, + selections, + ); + return; + }; + if condition_value == 0 { + break; + } + selected = true; + generate_selections_from_generate_block( + &generate.nodes.2, + syntax_tree, + &loop_env, + type_aliases, + selections, + ); + let Some(next) = next_genvar_value( + value, + &generate.nodes.1.nodes.1.4, + syntax_tree, + &loop_env, + type_aliases, + ) else { + break; + }; + value = next; + } + record_generate_block_selection(selections, &generate.nodes.2, selected); +} + +fn generate_selections_from_generate_block<'a>( + block: &'a sv_parser::GenerateBlock, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, +) { + match block { + sv_parser::GenerateBlock::GenerateItem(item) => { + generate_selections_from_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); + } + sv_parser::GenerateBlock::Multiple(block) => { + let mut block_env = const_env.clone(); + for item in &block.nodes.3 { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + type_aliases, + ) { + continue; + } + generate_selections_from_generate_item( + item, + syntax_tree, + &block_env, + type_aliases, + selections, + ); + } + } + } +} + +fn blocking_assignment_has_non_plain_lvalue(assignment: &sv_parser::BlockingAssignment) -> bool { + let lvalue = match assignment { + sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.0, + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => &assignment.nodes.0, + _ => return true, + }; + let sv_parser::VariableLvalue::Identifier(identifier) = lvalue else { + return true; + }; + let select = &identifier.nodes.2; + select.nodes.0.is_some() || !select.nodes.1.nodes.0.is_empty() || select.nodes.2.is_some() +} + +fn non_input_function_port(node: RefNode<'_>) -> bool { + let direction = match node { + RefNode::TfPortItem(port) => port.nodes.1.as_ref(), + RefNode::TfPortDeclaration(port) => Some(&port.nodes.1), + _ => return false, + }; + match direction { + None => false, + Some(sv_parser::TfPortDirection::PortDirection(direction)) => { + !matches!(&**direction, sv_parser::PortDirection::Input(_)) + } + Some(sv_parser::TfPortDirection::ConstRef(_)) => true, + } +} + +fn function_has_static_local_state(function: &sv_parser::FunctionDeclaration) -> bool { + let function_is_static = !matches!(function.nodes.1, Some(sv_parser::Lifetime::Automatic(_))); + let local_is_static = |item: &sv_parser::BlockItemDeclaration| { + let sv_parser::BlockItemDeclaration::Data(item) = item else { + return false; + }; + let sv_parser::DataDeclaration::Variable(variable) = &item.nodes.1 else { + return false; + }; + match &variable.nodes.2 { + Some(sv_parser::Lifetime::Automatic(_)) => false, + Some(sv_parser::Lifetime::Static(_)) => true, + None => function_is_static, + } + }; + match &function.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => { + body.nodes.5.iter().any(local_is_static) + } + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => body.nodes.4.iter().any(|item| { + matches!( + item, + sv_parser::TfItemDeclaration::BlockItemDeclaration(item) + if local_is_static(item) + ) + }), + } +} + +fn apply_parameter_overrides( + parameters: &mut [Parameter], + overrides: &HashMap, +) -> Result<(), AnalyzerError> { + if let Some(name) = overrides + .keys() + .find(|name| !parameters.iter().any(|parameter| parameter.name() == *name)) + { + return Err(AnalyzerError::Unsupported(format!( + "unknown top-level parameter override `{name}`" + ))); + } + if let Some(name) = overrides.keys().find(|name| { + parameters + .iter() + .any(|parameter| parameter.name() == *name && parameter.is_local) + }) { + return Err(AnalyzerError::Unsupported(format!( + "localparam override `{name}`" + ))); + } + for parameter in parameters { + if let Some(value) = overrides.get(parameter.name()) { + parameter.value = Some(value.clone()); + } + } + Ok(()) +} + +fn const_expr_from_i128(value: i128) -> ConstExpr { + if value < 0 { + ConstExpr::Unary { + op: UnaryOp::Minus, + expr: Box::new(ConstExpr::Literal(value.unsigned_abs().to_string())), + } + } else { + ConstExpr::Literal(value.to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Parameter { + name: String, + value: Option, + declared_width: Option, + declared_signed: Option, + declared_is_2state: bool, + has_declared_type: bool, + is_local: bool, +} + +impl Parameter { + fn new( + name: String, + value: Option, + declared_width: Option, + declared_signed: Option, + declared_is_2state: bool, + has_declared_type: bool, + is_local: bool, + ) -> Self { + Self { + name, + value, + declared_width, + declared_signed, + declared_is_2state, + has_declared_type, + is_local, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn value(&self) -> Option<&ConstExpr> { + self.value.as_ref() + } + + pub(crate) fn resolved_value( + &self, + constants: &HashMap, + parameter_types: &HashMap, + ) -> Option { + let mut value = + substitute_typed_parameter_literals(self.value()?.clone(), constants, parameter_types); + if self.declared_is_2state { + value = ConstExpr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(value), + }; + } + let mut value = typecheck::eval_const_expr(&value.into(), constants)?; + if let Some(width) = self.declared_width { + value = + coerce_const_parameter_value(value, width, self.declared_signed.unwrap_or(false)); + } + Some(value) + } + + pub(crate) fn resolved_type( + &self, + parameter_types: &HashMap, + ) -> Option { + let inferred = self.value().and_then(|value| { + infer_parameter_value_type(value, self.has_declared_type, parameter_types) + }); + let width = self + .declared_width + .or(inferred.map(|r#type| r#type.width))?; + let signed = self + .declared_signed + .or(inferred.map(|r#type| r#type.signed)) + .unwrap_or(false); + Some(ExprType { width, signed }) + } + + pub(crate) fn declared_width(&self) -> Option { + self.declared_width + } + + pub(crate) fn declared_signed(&self) -> Option { + self.declared_signed + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Port { + name: String, + direction: PortDirection, + r#type: Type, + is_net: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signal { + name: String, + r#type: Type, + is_net: bool, +} + +impl Signal { + fn new(name: String, r#type: Type) -> Self { + Self { + name, + r#type, + is_net: false, + } + } + + fn new_net(name: String, r#type: Type) -> Self { + Self { + name, + r#type, + is_net: true, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn r#type(&self) -> &Type { + &self.r#type + } + + pub(crate) fn is_net(&self) -> bool { + self.is_net + } +} + +impl Port { + fn new(name: String, direction: PortDirection, r#type: Type, is_net: bool) -> Self { + Self { + name, + direction, + r#type, + is_net, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn direction(&self) -> PortDirection { + self.direction + } + + pub fn r#type(&self) -> &Type { + &self.r#type + } + + pub fn is_net(&self) -> bool { + self.is_net + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Instance { + module_name: String, + name: String, + parameter_names: Vec, + parameter_overrides: Vec, + condition: Option, + port_names: Vec, + port_connections: Vec, +} + +impl Instance { + fn new( + module_name: String, + name: String, + parameter_names: Vec, + parameter_overrides: Vec, + condition: Option, + port_names: Vec, + port_connections: Vec, + ) -> Self { + Self { + module_name, + name, + parameter_names, + parameter_overrides, + condition, + port_names, + port_connections, + } + } + + pub fn module_name(&self) -> &str { + &self.module_name + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn parameter_names(&self) -> &[String] { + &self.parameter_names + } + + pub fn parameter_overrides(&self) -> &[ParameterOverride] { + &self.parameter_overrides + } + + pub fn condition(&self) -> Option<&ConstExpr> { + self.condition.as_ref() + } + + pub fn port_names(&self) -> &[String] { + &self.port_names + } + + pub fn port_connections(&self) -> &[PortConnection] { + &self.port_connections + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParameterOverride { + name: String, + value: Option, +} + +impl ParameterOverride { + fn new(name: String, value: Option) -> Self { + Self { name, value } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn value(&self) -> Option<&ConstExpr> { + self.value.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PortConnection { + formal: String, + actual: String, + actual_expr: Option, +} + +impl PortConnection { + fn new(formal: String, actual: String, actual_expr: Option) -> Self { + Self { + formal, + actual, + actual_expr, + } + } + + pub fn formal(&self) -> &str { + &self.formal + } + + pub fn actual(&self) -> &str { + &self.actual + } + + pub fn actual_expr(&self) -> Option<&Expr> { + self.actual_expr.as_ref() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PortDirection { + Input, + Output, + Inout, + Ref, + Unspecified, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Type { + kind: TypeKind, + is_signed: bool, + packed_ranges: Vec, + unpacked_ranges: Vec, +} + +impl Type { + fn implicit() -> Self { + Self { + kind: TypeKind::Implicit, + is_signed: false, + packed_ranges: Vec::new(), + unpacked_ranges: Vec::new(), + } + } + + fn new(kind: TypeKind) -> Self { + Self { + kind, + is_signed: false, + packed_ranges: Vec::new(), + unpacked_ranges: Vec::new(), + } + } + + pub fn kind(&self) -> TypeKind { + self.kind + } + + pub fn is_signed(&self) -> bool { + self.is_signed + } + + pub fn packed_ranges(&self) -> &[PackedRange] { + &self.packed_ranges + } + + pub fn unpacked_ranges(&self) -> &[UnpackedRange] { + &self.unpacked_ranges + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeKind { + Bit, + Logic, + Reg, + Implicit, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedRange { + left: ConstExpr, + right: ConstExpr, +} + +impl PackedRange { + fn new(left: ConstExpr, right: ConstExpr) -> Self { + Self { left, right } + } + + pub fn left(&self) -> &ConstExpr { + &self.left + } + + pub fn right(&self) -> &ConstExpr { + &self.right + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnpackedRange { + left: ConstExpr, + right: ConstExpr, + size: Option, +} + +impl UnpackedRange { + fn new(left: ConstExpr, right: ConstExpr) -> Self { + Self { + left, + right, + size: None, + } + } + + fn sized(size: ConstExpr) -> Self { + Self { + left: ConstExpr::Literal("0".to_string()), + right: ConstExpr::Binary { + left: Box::new(size.clone()), + op: BinaryOp::Sub, + right: Box::new(ConstExpr::Literal("1".to_string())), + }, + size: Some(size), + } + } + + pub fn left(&self) -> &ConstExpr { + &self.left + } + + pub fn right(&self) -> &ConstExpr { + &self.right + } + + fn size(&self) -> Option<&ConstExpr> { + self.size.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConstExpr { + Literal(String), + Ident(String), + Select { + expr: Box, + bit: Box, + }, + Function { + name: String, + args: Vec, + }, + Unary { + op: UnaryOp, + expr: Box, + }, + Binary { + left: Box, + op: BinaryOp, + right: Box, + }, + Mux { + condition: Box, + then_expr: Box, + else_expr: Box, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + Plus, + Minus, + BitNot, + LogicNot, + ToTwoState, + RedAnd, + RedOr, + RedXor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + Shl, + Shr, + Sar, + BitAnd, + BitOr, + BitXor, + LogicAnd, + LogicOr, + Eq, + Ne, + EqCase, + NeCase, + EqWildcard, + NeWildcard, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assignment { + lhs: LValue, + rhs: Expr, +} + +impl Assignment { + fn new(lhs: LValue, rhs: Expr) -> Self { + Self { lhs, rhs } + } + + pub fn lhs(&self) -> &str { + self.lhs.name() + } + + pub fn lhs_value(&self) -> &LValue { + &self.lhs + } + + pub fn rhs(&self) -> &Expr { + &self.rhs + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LValue { + Ident(String), + Select { + name: String, + msb: ConstExpr, + lsb: ConstExpr, + signed: bool, + array_slice_width: Option, + array_slice_reversed: bool, + }, +} + +impl LValue { + pub fn name(&self) -> &str { + match self { + LValue::Ident(name) | LValue::Select { name, .. } => name, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CombProcess { + kind: CombProcessKind, + condition: Option, + assignments: Vec, +} + +impl CombProcess { + fn new( + kind: CombProcessKind, + condition: Option, + assignments: Vec, + ) -> Self { + Self { + kind, + condition, + assignments, + } + } + + pub fn kind(&self) -> CombProcessKind { + self.kind + } + + pub fn condition(&self) -> Option<&ConstExpr> { + self.condition.as_ref() + } + + pub fn assignments(&self) -> &[Assignment] { + &self.assignments + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CombProcessKind { + ContinuousAssign, + AlwaysComb, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FfProcess { + events: Vec, + assignments: Vec, +} + +impl FfProcess { + fn new(events: Vec, assignments: Vec) -> Self { + Self { + events, + assignments, + } + } + + pub fn events(&self) -> &[FfEvent] { + &self.events + } + + pub fn assignments(&self) -> &[ConditionalAssignment] { + &self.assignments + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FfEdge { + Pos, + Neg, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FfEvent { + edge: FfEdge, + signal: String, +} + +impl FfEvent { + fn new(edge: FfEdge, signal: String) -> Self { + Self { edge, signal } + } + + pub fn edge(&self) -> FfEdge { + self.edge + } + + pub fn signal(&self) -> &str { + &self.signal + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConditionalAssignment { + condition: Option, + assignment: Assignment, + exhaustive_fallback_start: Option, + /// Statement slot at which the controlling if/case chain was evaluated. + guard_boundary: Option, + /// Nested branch paths, from innermost to outermost. + path_epochs: Vec, +} + +impl ConditionalAssignment { + fn new(condition: Option, assignment: Assignment) -> Self { + Self { + condition, + assignment, + exhaustive_fallback_start: None, + guard_boundary: None, + path_epochs: Vec::new(), + } + } + + pub fn condition(&self) -> Option<&Expr> { + self.condition.as_ref() + } + + pub fn assignment(&self) -> &Assignment { + &self.assignment + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Function { + name: String, + params: Vec, + body: Expr, + return_width: Option, + return_first_packed_dimension_width: Option, + return_signed: bool, + return_is_2state: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct FunctionParam { + name: String, + width: Option, + signed: bool, + is_2state: bool, + packed_dimensions: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct FunctionLocalType { + width: usize, + signed: bool, + is_2state: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Expr { + Ident(String), + Literal(String), + Select { + expr: Box, + msb: ConstExpr, + lsb: ConstExpr, + signed: bool, + }, + Concat(Vec), + RepeatConcat { + count: ConstExpr, + parts: Vec, + }, + Resize { + expr: Box, + width: usize, + signed: bool, + }, + Unary { + op: UnaryOp, + expr: Box, + }, + Binary { + left: Box, + op: BinaryOp, + right: Box, + }, + Mux { + condition: Box, + then_expr: Box, + else_expr: Box, + }, + Call { + name: String, + args: Vec, + }, +} + +fn identifier_locate(node: RefNode<'_>) -> Option { + match unwrap_node!(node, SimpleIdentifier, EscapedIdentifier) { + Some(RefNode::SimpleIdentifier(identifier)) => Some(identifier.nodes.0), + Some(RefNode::EscapedIdentifier(identifier)) => Some(identifier.nodes.0), + _ => None, + } +} + +fn ports_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Result, AnalyzerError> { + let mut ports = Vec::new(); + let mut inherited_direction = PortDirection::Unspecified; + let mut inherited_type = Type::implicit(); + for child in node { + match child { + RefNode::AnsiPortDeclarationNet(port) => { + let header = port.nodes.0.as_ref(); + let direction = header + .and_then(|header| direction_from_ref_node(header.into())) + .unwrap_or(inherited_direction); + let r#type = match header { + Some(header) => { + type_from_net_port_header(header, syntax_tree, const_env, type_aliases) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported port data type".to_string()) + })? + } + None => inherited_type.clone(), + }; + let r#type = type_with_fallback_ranges_with_env( + r#type, + RefNode::AnsiPortDeclarationNet(port), + syntax_tree, + const_env, + type_aliases, + ); + let inherited_type_base = r#type.clone(); + let r#type = type_with_unpacked_ranges( + r#type, + unpacked_ranges_from_dimensions_with_env( + &port.nodes.2, + syntax_tree, + const_env, + type_aliases, + )?, + ); + let name = port_name(RefNode::PortIdentifier(&port.nodes.1), syntax_tree)?; + inherited_direction = direction; + inherited_type = inherited_type_base; + ports.push(Port::new(name, direction, r#type, true)); + } + RefNode::AnsiPortDeclarationVariable(port) => { + let header = port.nodes.0.as_ref(); + let direction = header + .and_then(|header| direction_from_ref_node(header.into())) + .unwrap_or(inherited_direction); + let r#type = match header { + Some(header) => { + type_from_variable_port_header(header, syntax_tree, const_env, type_aliases) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported port data type".to_string()) + })? + } + None => inherited_type.clone(), + }; + let r#type = type_with_fallback_ranges_with_env( + r#type, + RefNode::AnsiPortDeclarationVariable(port), syntax_tree, - ) - .is_none() - || const_expr_from_ref_node( - RefNode::ConstantExpression(&range.nodes.0.nodes.1.nodes.2), + const_env, + type_aliases, + ); + let inherited_type_base = r#type.clone(); + let r#type = type_with_unpacked_ranges( + r#type, + unpacked_ranges_from_variable_dimensions_with_env( + &port.nodes.2, syntax_tree, - ) - .is_none() => - { - return Err(AnalyzerError::Unsupported( - "unsupported packed range".to_string(), - )); + const_env, + type_aliases, + )?, + ); + let name = port_name(RefNode::PortIdentifier(&port.nodes.1), syntax_tree)?; + inherited_direction = direction; + inherited_type = inherited_type_base; + ports.push(Port::new(name, direction, r#type, false)); } - RefNode::PackageImportDeclaration(_) | RefNode::PackageScope(_) => { - return Err(AnalyzerError::Unsupported( - "package-dependent systemverilog module".to_string(), - )); + RefNode::AnsiPortDeclarationParen(port) => { + let explicit_direction = port.nodes.0.as_ref().map(direction_from_port_direction); + let direction = explicit_direction.unwrap_or(inherited_direction); + let r#type = if explicit_direction.is_some() { + Type::implicit() + } else { + inherited_type.clone() + }; + let name = port_name(RefNode::PortIdentifier(&port.nodes.2), syntax_tree)?; + inherited_direction = direction; + inherited_type = r#type.clone(); + ports.push(Port::new(name, direction, r#type, false)); } - RefNode::ParamAssignment(parameter) - if RefNode::ParamAssignment(parameter).into_iter().any(|node| { - matches!( - node, - RefNode::ConstantExpression( - sv_parser::ConstantExpression::Unary(unary) - ) if syntax_tree - .get_str(&unary.nodes.0.nodes.0.nodes.0) - .is_some_and(|op| op == "~") - && matches!( - const_expr_from_ref_node( - RefNode::ConstantPrimary(&unary.nodes.2), - syntax_tree, - ), - Some(ConstExpr::Ident(_)) - ) - ) - }) => - { - return Err(AnalyzerError::Unsupported( - "width-dependent complement in parameter expression".to_string(), - )); + _ => {} + } + } + Ok(ports) +} + +fn parameters_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, + base_const_env: &HashMap, + parameter_overrides: &HashMap, +) -> Result, AnalyzerError> { + let mut parameters = Vec::new(); + if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { + let RefNode::ParameterPortList(parameter_port_list) = parameter_port_list else { + unreachable!(); + }; + parameters_from_parameter_port_list( + parameter_port_list, + syntax_tree, + &mut parameters, + base_const_env, + type_aliases, + parameter_overrides, + )?; + } + + for item in module_non_port_items(node.clone()) { + if let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) { + match declaration { + sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration( + localparam, + ) => parameters_from_ref_node( + RefNode::LocalParameterDeclaration(&localparam.0), + syntax_tree, + &mut parameters, + true, + base_const_env, + type_aliases, + parameter_overrides, + )?, + sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { + parameters_from_ref_node( + RefNode::ParameterDeclaration(¶meter.0), + syntax_tree, + &mut parameters, + false, + base_const_env, + type_aliases, + parameter_overrides, + )? + } + _ => {} } - RefNode::ParamAssignment(parameter) - if RefNode::ParamAssignment(parameter).into_iter().any(|node| { - matches!( - node, - RefNode::UnaryOperator(operator) - if syntax_tree - .get_str(&operator.nodes.0) - .is_some_and(|op| matches!(op, "&" | "|" | "^" | "~&" | "~|" | "~^" | "^~")) - ) - }) => - { - return Err(AnalyzerError::Unsupported( - "reduction operator in parameter expression".to_string(), - )); + } + } + + Ok(parameters) +} + +fn parameters_from_parameter_port_list( + list: &sv_parser::ParameterPortList, + syntax_tree: &SyntaxTree, + parameters: &mut Vec, + base_const_env: &HashMap, + type_aliases: &HashMap, + parameter_overrides: &HashMap, +) -> Result<(), AnalyzerError> { + match list { + sv_parser::ParameterPortList::Assignment(list) => { + parameters_from_ref_node( + RefNode::ListOfParamAssignments(&list.nodes.1.nodes.1.0), + syntax_tree, + parameters, + false, + base_const_env, + type_aliases, + parameter_overrides, + )?; + for (_, declaration) in &list.nodes.1.nodes.1.1 { + parameters_from_parameter_port_declaration( + declaration, + syntax_tree, + parameters, + base_const_env, + type_aliases, + parameter_overrides, + )?; + } + } + sv_parser::ParameterPortList::Declaration(list) => { + for declaration in list.nodes.1.nodes.1.contents() { + parameters_from_parameter_port_declaration( + declaration, + syntax_tree, + parameters, + base_const_env, + type_aliases, + parameter_overrides, + )?; } - _ => {} } + sv_parser::ParameterPortList::Empty(_) => {} } Ok(()) } -fn inactive_conditional_generate_nodes<'a>( - node: RefNode<'a>, +fn parameters_from_parameter_port_declaration( + declaration: &sv_parser::ParameterPortDeclaration, syntax_tree: &SyntaxTree, - const_env: &HashMap, -) -> Vec> { - let mut selections = Vec::new(); - for item in module_non_port_items(node) { - generate_selections_from_non_port_item(item, syntax_tree, const_env, &mut selections); + parameters: &mut Vec, + base_const_env: &HashMap, + type_aliases: &HashMap, + parameter_overrides: &HashMap, +) -> Result<(), AnalyzerError> { + let is_local = matches!( + declaration, + sv_parser::ParameterPortDeclaration::LocalParameterDeclaration(_) + ); + if matches!( + declaration, + sv_parser::ParameterPortDeclaration::TypeList(_) + ) { + return Ok(()); } - selections - .into_iter() - .filter(|(_, selected)| !selected) - .flat_map(|(block, _)| RefNode::GenerateBlock(block)) - .collect() + parameters_from_ref_node( + RefNode::ParameterPortDeclaration(declaration), + syntax_tree, + parameters, + is_local, + base_const_env, + type_aliases, + parameter_overrides, + ) } -fn record_generate_block_selection<'a>( - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, - block: &'a sv_parser::GenerateBlock, - selected: bool, -) { - if let Some((_, previously_selected)) = selections - .iter_mut() - .find(|(candidate, _)| *candidate == block) +fn signals_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Result, AnalyzerError> { + let mut signals = Vec::new(); + for item in module_non_port_items(node) { + signals_from_non_port_module_item( + item, + syntax_tree, + type_aliases, + const_env, + &mut signals, + )?; + } + signals.sort_by(|a, b| a.name.cmp(&b.name)); + if let Some(name) = signals + .windows(2) + .find(|pair| pair[0].name == pair[1].name) + .map(|pair| pair[0].name.clone()) { - *previously_selected |= selected; - } else { - selections.push((block, selected)); + return Err(AnalyzerError::Unsupported(format!( + "duplicate internal signal `{name}`" + ))); } + Ok(signals) } -fn generate_selections_from_non_port_item<'a>( - item: &'a sv_parser::NonPortModuleItem, +fn signals_from_non_port_module_item( + item: &sv_parser::NonPortModuleItem, syntax_tree: &SyntaxTree, + type_aliases: &HashMap, const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { + signals: &mut Vec, +) -> Result<(), AnalyzerError> { match item { sv_parser::NonPortModuleItem::GenerateRegion(region) => { for item in ®ion.nodes.1 { - generate_selections_from_generate_item(item, syntax_tree, const_env, selections); + signals_from_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; } } sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { - generate_selections_from_module_or_generate_item( + signals_from_module_or_generate_item( item, syntax_tree, + type_aliases, const_env, - selections, - ); + signals, + )?; } _ => {} } + Ok(()) } -fn generate_selections_from_generate_item<'a>( - item: &'a sv_parser::GenerateItem, +fn signals_from_generate_item( + item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, + type_aliases: &HashMap, const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { + signals: &mut Vec, +) -> Result<(), AnalyzerError> { if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { - generate_selections_from_module_or_generate_item(item, syntax_tree, const_env, selections); + signals_from_module_or_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; } + Ok(()) } -fn generate_selections_from_module_or_generate_item<'a>( - item: &'a sv_parser::ModuleOrGenerateItem, +fn signals_from_module_or_generate_item( + item: &sv_parser::ModuleOrGenerateItem, syntax_tree: &SyntaxTree, + type_aliases: &HashMap, const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { - let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item else { - return; - }; - match &item.nodes.1 { + signals: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::ModuleOrGenerateItem::Module(module) => { + let mut alias_signals = signals_from_type_alias_instantiation( + &module.nodes.1, + syntax_tree, + type_aliases, + const_env, + )?; + substitute_signal_local_constants(&mut alias_signals, const_env); + signals.extend(alias_signals); + } + sv_parser::ModuleOrGenerateItem::ModuleItem(item) => { + signals_from_module_common_item( + &item.nodes.1, + syntax_tree, + type_aliases, + const_env, + signals, + )?; + } + _ => {} + } + Ok(()) +} + +fn signals_from_module_common_item( + item: &sv_parser::ModuleCommonItem, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, + const_env: &HashMap, + signals: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) => { + let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration( + declaration, + ) = &**declaration + else { + return Ok(()); + }; + let mut declared = match &**declaration { + sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) => { + signals_from_data_declaration(data, syntax_tree, type_aliases, const_env)? + } + sv_parser::PackageOrGenerateItemDeclaration::NetDeclaration(net) => { + signals_from_net_declaration(net, syntax_tree, type_aliases, const_env)? + } + _ => Vec::new(), + }; + substitute_signal_local_constants(&mut declared, const_env); + signals.extend(declared); + } sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { - generate_selections_from_conditional_generate( - generate, + let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { + return Ok(()); + }; + let condition = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1), + syntax_tree, + const_env, + type_aliases, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("conditional-generate condition lowering".to_string()) + })?; + let condition = eval_ast_const_expr(&condition, const_env).ok_or_else(|| { + AnalyzerError::Unsupported("unknown conditional-generate condition".to_string()) + })?; + let block = if condition != 0 { + Some(&generate.nodes.2) + } else { + generate.nodes.3.as_ref().map(|(_, block)| block) + }; + if let Some(block) = block { + signals_from_generate_block(block, syntax_tree, type_aliases, const_env, signals)?; + } + } + _ => {} + } + Ok(()) +} + +fn signals_from_generate_block( + block: &sv_parser::GenerateBlock, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, + const_env: &HashMap, + signals: &mut Vec, +) -> Result<(), AnalyzerError> { + match block { + sv_parser::GenerateBlock::GenerateItem(item) => { + signals_from_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; + } + sv_parser::GenerateBlock::Multiple(block) => { + let mut block_env = const_env.clone(); + for item in &block.nodes.3 { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + type_aliases, + ) { + continue; + } + signals_from_generate_item(item, syntax_tree, type_aliases, &block_env, signals)?; + } + } + } + Ok(()) +} + +fn substitute_signal_local_constants(signals: &mut [Signal], const_env: &HashMap) { + for signal in signals { + for range in &mut signal.r#type.packed_ranges { + range.left = substitute_dimension_constants(range.left.clone(), const_env); + range.right = substitute_dimension_constants(range.right.clone(), const_env); + } + for range in &mut signal.r#type.unpacked_ranges { + range.left = substitute_dimension_constants(range.left.clone(), const_env); + range.right = substitute_dimension_constants(range.right.clone(), const_env); + range.size = range + .size + .take() + .map(|size| substitute_dimension_constants(size, const_env)); + } + } +} + +fn signals_from_net_declaration( + net: &sv_parser::NetDeclaration, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, + const_env: &HashMap, +) -> Result, AnalyzerError> { + let (r#type, assignments, is_net) = match net { + sv_parser::NetDeclaration::NetType(net) => { + let r#type = type_from_ref_node_with_env( + RefNode::DataTypeOrImplicit(&net.nodes.3), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| { + type_alias_from_ref_node( + RefNode::DataTypeOrImplicit(&net.nodes.3), + syntax_tree, + type_aliases, + ) + }); + let r#type = match (&net.nodes.3, r#type) { + (_, Some(r#type)) => r#type, + (sv_parser::DataTypeOrImplicit::ImplicitDataType(_), None) => Type::implicit(), + (sv_parser::DataTypeOrImplicit::DataType(_), None) => { + return Err(AnalyzerError::Unsupported( + "unsupported net data type".to_string(), + )); + } + }; + let r#type = type_with_fallback_ranges_with_env( + r#type, + RefNode::DataTypeOrImplicit(&net.nodes.3), syntax_tree, const_env, - selections, + type_aliases, ); + (r#type, net.nodes.5.nodes.0.contents(), true) } - sv_parser::ModuleCommonItem::LoopGenerateConstruct(generate) => { - generate_selections_from_loop_generate(generate, syntax_tree, const_env, selections); + sv_parser::NetDeclaration::NetTypeIdentifier(net) => { + let Some(name) = identifier_text(RefNode::NetTypeIdentifier(&net.nodes.0), syntax_tree) + else { + return Ok(Vec::new()); + }; + let Some(r#type) = type_aliases.get(&name).cloned() else { + return Ok(Vec::new()); + }; + (r#type, net.nodes.2.nodes.0.contents(), false) + } + sv_parser::NetDeclaration::Interconnect(_) => { + return Err(AnalyzerError::Unsupported( + "interconnect net declaration".to_string(), + )); } - _ => {} - } -} - -fn generate_selections_from_conditional_generate<'a>( - generate: &'a sv_parser::ConditionalGenerateConstruct, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { - let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { - return; }; - let condition = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1), - syntax_tree, - ) - .and_then(|condition| eval_ast_const_expr(&condition, const_env)); - let then_selected = condition.map(|condition| condition != 0).unwrap_or(true); - record_generate_block_selection(selections, &generate.nodes.2, then_selected); - if then_selected { - generate_selections_from_generate_block( - &generate.nodes.2, - syntax_tree, - const_env, - selections, + let mut signals = Vec::new(); + for assignment in assignments { + let name = identifier_text(RefNode::NetIdentifier(&assignment.nodes.0), syntax_tree) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported signal identifier".to_string()) + })?; + let signal_type = type_with_unpacked_ranges( + r#type.clone(), + unpacked_ranges_from_dimensions_with_env( + &assignment.nodes.1, + syntax_tree, + const_env, + type_aliases, + )?, ); + signals.push(if is_net { + Signal::new_net(name, signal_type) + } else { + Signal::new(name, signal_type) + }); } - if let Some((_, block)) = &generate.nodes.3 { - let else_selected = condition.map(|condition| condition == 0).unwrap_or(true); - record_generate_block_selection(selections, block, else_selected); - if else_selected { - generate_selections_from_generate_block(block, syntax_tree, const_env, selections); - } - } + Ok(signals) } -fn generate_selections_from_loop_generate<'a>( - generate: &'a sv_parser::LoopGenerateConstruct, +fn signals_from_type_alias_instantiation( + instantiation: &sv_parser::ModuleInstantiation, syntax_tree: &SyntaxTree, + type_aliases: &HashMap, const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { - let Some(name) = identifier_text( - RefNode::GenvarIdentifier(&generate.nodes.1.nodes.1.0.nodes.1), - syntax_tree, - ) else { - record_generate_block_selection(selections, &generate.nodes.2, true); - generate_selections_from_generate_block( - &generate.nodes.2, - syntax_tree, - const_env, - selections, - ); - return; - }; - let Some(init) = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1.0.nodes.3), +) -> Result, AnalyzerError> { + let mut signals = Vec::new(); + let module_name = identifier_text( + RefNode::ModuleIdentifier(&instantiation.nodes.0), syntax_tree, ) - .and_then(|init| eval_ast_const_expr(&init, const_env)) else { - record_generate_block_selection(selections, &generate.nodes.2, true); - generate_selections_from_generate_block( - &generate.nodes.2, - syntax_tree, - const_env, - selections, - ); - return; + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) + })?; + let Some(r#type) = type_aliases.get(&module_name) else { + return Ok(signals); }; - let Some(condition) = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1.2.nodes.0), - syntax_tree, - ) else { - record_generate_block_selection(selections, &generate.nodes.2, true); - generate_selections_from_generate_block( - &generate.nodes.2, + for instance in instantiation.nodes.2.contents() { + let name = identifier_text( + RefNode::InstanceIdentifier(&instance.nodes.0.nodes.0), syntax_tree, - const_env, - selections, - ); - return; - }; - - let mut value = init; - let mut selected = false; - for _ in 0..10_000 { - let mut loop_env = const_env.clone(); - loop_env.insert(name.clone(), value); - let Some(condition_value) = eval_ast_const_expr(&condition, &loop_env) else { - record_generate_block_selection(selections, &generate.nodes.2, true); - generate_selections_from_generate_block( - &generate.nodes.2, + ) + .ok_or_else(|| AnalyzerError::Unsupported("unsupported signal identifier".to_string()))?; + let signal_type = type_with_unpacked_ranges( + r#type.clone(), + unpacked_ranges_from_dimensions_with_env( + &instance.nodes.0.nodes.1, syntax_tree, - &loop_env, - selections, - ); - return; - }; - if condition_value == 0 { - break; - } - selected = true; - generate_selections_from_generate_block( - &generate.nodes.2, - syntax_tree, - &loop_env, - selections, + const_env, + type_aliases, + )?, ); - let Some(next) = - next_genvar_value(value, &generate.nodes.1.nodes.1.4, syntax_tree, &loop_env) - else { - break; - }; - value = next; + signals.push(Signal::new(name, signal_type)); } - record_generate_block_selection(selections, &generate.nodes.2, selected); + Ok(signals) } -fn generate_selections_from_generate_block<'a>( - block: &'a sv_parser::GenerateBlock, +fn type_aliases_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, +) -> Result, AnalyzerError> { + type_aliases_from_module_node_with_env(node, syntax_tree, &HashMap::default()) +} + +fn type_aliases_from_module_node_with_env( + node: RefNode<'_>, syntax_tree: &SyntaxTree, const_env: &HashMap, - selections: &mut Vec<(&'a sv_parser::GenerateBlock, bool)>, -) { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => { - generate_selections_from_generate_item(item, syntax_tree, const_env, selections); +) -> Result, AnalyzerError> { + let mut aliases = HashMap::default(); + if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { + if let RefNode::ParameterPortList(parameter_port_list) = parameter_port_list { + add_type_aliases_from_parameter_port_list( + parameter_port_list, + syntax_tree, + const_env, + &mut aliases, + ); } - sv_parser::GenerateBlock::Multiple(block) => { - let mut block_env = const_env.clone(); - for item in &block.nodes.3 { - if add_localparams_from_generate_item(item, syntax_tree, &mut block_env) { - continue; + for child in parameter_port_list { + match child { + RefNode::LocalParameterDeclaration(localparam) => { + add_type_aliases_from_localparam( + localparam, + syntax_tree, + const_env, + &mut aliases, + ); + } + RefNode::ParameterDeclaration(parameter) => { + add_type_aliases_from_parameter( + parameter, + syntax_tree, + const_env, + &mut aliases, + ); } - generate_selections_from_generate_item(item, syntax_tree, &block_env, selections); + _ => {} } } } -} - -fn blocking_assignment_has_non_plain_lvalue(assignment: &sv_parser::BlockingAssignment) -> bool { - let lvalue = match assignment { - sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.0, - sv_parser::BlockingAssignment::OperatorAssignment(assignment) => &assignment.nodes.0, - _ => return true, - }; - let sv_parser::VariableLvalue::Identifier(identifier) = lvalue else { - return true; - }; - let select = &identifier.nodes.2; - select.nodes.0.is_some() || !select.nodes.1.nodes.0.is_empty() || select.nodes.2.is_some() -} - -fn non_input_function_port(node: RefNode<'_>) -> bool { - let direction = match node { - RefNode::TfPortItem(port) => port.nodes.1.as_ref(), - RefNode::TfPortDeclaration(port) => Some(&port.nodes.1), - _ => return false, - }; - match direction { - None => false, - Some(sv_parser::TfPortDirection::PortDirection(direction)) => { - !matches!(&**direction, sv_parser::PortDirection::Input(_)) - } - Some(sv_parser::TfPortDirection::ConstRef(_)) => true, - } -} - -fn function_has_static_local_state(function: &sv_parser::FunctionDeclaration) -> bool { - let function_is_static = !matches!(function.nodes.1, Some(sv_parser::Lifetime::Automatic(_))); - let local_is_static = |item: &sv_parser::BlockItemDeclaration| { - let sv_parser::BlockItemDeclaration::Data(item) = item else { - return false; - }; - let sv_parser::DataDeclaration::Variable(variable) = &item.nodes.1 else { - return false; + for item in module_non_port_items(node.clone()) { + let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { + continue; }; - match &variable.nodes.2 { - Some(sv_parser::Lifetime::Automatic(_)) => false, - Some(sv_parser::Lifetime::Static(_)) => true, - None => function_is_static, - } - }; - match &function.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => { - body.nodes.5.iter().any(local_is_static) + match declaration { + sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(declaration) => { + add_type_alias_from_data_declaration( + declaration, + syntax_tree, + const_env, + &mut aliases, + )?; + } + sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) => { + add_type_aliases_from_localparam( + &localparam.0, + syntax_tree, + const_env, + &mut aliases, + ); + } + sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { + add_type_aliases_from_parameter(¶meter.0, syntax_tree, const_env, &mut aliases); + } + _ => {} } - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => body.nodes.4.iter().any(|item| { - matches!( - item, - sv_parser::TfItemDeclaration::BlockItemDeclaration(item) - if local_is_static(item) - ) - }), } + for child in node { + let RefNode::TypeAssignment(assignment) = child else { + continue; + }; + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, &mut aliases); + } + Ok(aliases) } -fn apply_parameter_overrides( - parameters: &mut [Parameter], - overrides: &HashMap, +fn add_type_alias_from_data_declaration( + declaration: &sv_parser::DataDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, ) -> Result<(), AnalyzerError> { - if let Some(name) = overrides - .keys() - .find(|name| !parameters.iter().any(|parameter| parameter.name() == *name)) - { - return Err(AnalyzerError::Unsupported(format!( - "unknown top-level parameter override `{name}`" - ))); - } - if let Some(name) = overrides.keys().find(|name| { - parameters - .iter() - .any(|parameter| parameter.name() == *name && parameter.is_local) - }) { - return Err(AnalyzerError::Unsupported(format!( - "localparam override `{name}`" - ))); - } - for parameter in parameters { - if let Some(value) = overrides.get(parameter.name()) { - parameter.value = Some(value.clone()); + let sv_parser::DataDeclaration::TypeDeclaration(declaration) = declaration else { + return Ok(()); + }; + let sv_parser::TypeDeclaration::DataType(declaration) = &**declaration else { + return Ok(()); + }; + let Some(name) = identifier_text(RefNode::TypeIdentifier(&declaration.nodes.2), syntax_tree) + else { + return Ok(()); + }; + let r#type = if let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 { + if let Some(base) = &r#enum.nodes.1 { + type_from_ref_node_with_env( + RefNode::EnumBaseType(base), + syntax_tree, + const_env, + aliases, + ) + .or_else(|| type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, aliases)) + } else { + let mut r#type = Type::new(TypeKind::Bit); + r#type.is_signed = true; + r#type.packed_ranges.push(PackedRange::new( + ConstExpr::Literal("31".to_string()), + ConstExpr::Literal("0".to_string()), + )); + Some(r#type) } - } + } else { + type_from_ref_node_with_env( + RefNode::DataType(&declaration.nodes.1), + syntax_tree, + const_env, + aliases, + ) + }; + let Some(r#type) = r#type else { + return Ok(()); + }; + let r#type = type_with_unpacked_ranges( + r#type, + unpacked_ranges_from_variable_dimensions_with_env( + &declaration.nodes.3, + syntax_tree, + const_env, + aliases, + )?, + ); + aliases.insert(name, r#type); Ok(()) } -fn const_expr_from_i128(value: i128) -> ConstExpr { - if value < 0 { - ConstExpr::Unary { - op: UnaryOp::Minus, - expr: Box::new(ConstExpr::Literal(value.unsigned_abs().to_string())), +fn add_type_aliases_from_parameter_port_list( + list: &sv_parser::ParameterPortList, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, +) { + match list { + sv_parser::ParameterPortList::Assignment(list) => { + for (_, declaration) in &list.nodes.1.nodes.1.1 { + add_type_aliases_from_parameter_port_declaration( + declaration, + syntax_tree, + const_env, + aliases, + ); + } } - } else { - ConstExpr::Literal(value.to_string()) + sv_parser::ParameterPortList::Declaration(list) => { + for declaration in list.nodes.1.nodes.1.contents() { + add_type_aliases_from_parameter_port_declaration( + declaration, + syntax_tree, + const_env, + aliases, + ); + } + } + sv_parser::ParameterPortList::Empty(_) => {} } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Parameter { - name: String, - value: Option, - declared_width: Option, - declared_signed: Option, - declared_is_2state: bool, - has_declared_type: bool, - is_local: bool, -} - -impl Parameter { - fn new( - name: String, - value: Option, - declared_width: Option, - declared_signed: Option, - declared_is_2state: bool, - has_declared_type: bool, - is_local: bool, - ) -> Self { - Self { - name, - value, - declared_width, - declared_signed, - declared_is_2state, - has_declared_type, - is_local, +fn add_type_aliases_from_parameter_port_declaration( + declaration: &sv_parser::ParameterPortDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, +) { + match declaration { + sv_parser::ParameterPortDeclaration::ParameterDeclaration(declaration) => { + add_type_aliases_from_parameter(declaration, syntax_tree, const_env, aliases); } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn value(&self) -> Option<&ConstExpr> { - self.value.as_ref() - } - - pub(crate) fn resolved_value( - &self, - constants: &HashMap, - parameter_types: &HashMap, - ) -> Option { - let mut value = - substitute_typed_parameter_literals(self.value()?.clone(), constants, parameter_types); - if self.declared_is_2state { - value = ConstExpr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(value), - }; + sv_parser::ParameterPortDeclaration::LocalParameterDeclaration(declaration) => { + add_type_aliases_from_localparam(declaration, syntax_tree, const_env, aliases); } - let mut value = typecheck::eval_const_expr(&value.into(), constants)?; - if let Some(width) = self.declared_width { - value = - coerce_const_parameter_value(value, width, self.declared_signed.unwrap_or(false)); + sv_parser::ParameterPortDeclaration::TypeList(list) => { + for assignment in list.nodes.1.nodes.0.contents() { + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); + } } - Some(value) + sv_parser::ParameterPortDeclaration::ParamList(_) => {} } +} - pub(crate) fn resolved_type( - &self, - parameter_types: &HashMap, - ) -> Option { - let inferred = self.value().and_then(|value| { - infer_parameter_value_type(value, self.has_declared_type, parameter_types) - }); - let width = self - .declared_width - .or(inferred.map(|r#type| r#type.width))?; - let signed = self - .declared_signed - .or(inferred.map(|r#type| r#type.signed)) - .unwrap_or(false); - Some(ExprType { width, signed }) +fn add_type_aliases_from_localparam( + declaration: &sv_parser::LocalParameterDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, +) { + let sv_parser::LocalParameterDeclaration::Type(declaration) = declaration else { + return; + }; + for assignment in declaration.nodes.2.nodes.0.contents() { + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); } +} - pub(crate) fn declared_width(&self) -> Option { - self.declared_width +fn add_type_aliases_from_parameter( + declaration: &sv_parser::ParameterDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, +) { + let sv_parser::ParameterDeclaration::Type(declaration) = declaration else { + return; + }; + for assignment in declaration.nodes.2.nodes.0.contents() { + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); } +} - pub(crate) fn declared_signed(&self) -> Option { - self.declared_signed +fn add_type_alias_from_type_assignment( + assignment: &sv_parser::TypeAssignment, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + aliases: &mut HashMap, +) { + let Some((_, data_type)) = &assignment.nodes.1 else { + return; + }; + let Some(name) = identifier_text(RefNode::TypeIdentifier(&assignment.nodes.0), syntax_tree) + else { + return; + }; + let Some(r#type) = type_from_ref_node_with_env( + RefNode::DataType(data_type), + syntax_tree, + const_env, + aliases, + ) else { + return; + }; + aliases.insert(name, r#type); +} + +fn signals_from_data_declaration( + data: &sv_parser::DataDeclaration, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, + const_env: &HashMap, +) -> Result, AnalyzerError> { + let sv_parser::DataDeclaration::Variable(variable) = data else { + return Ok(Vec::new()); + }; + let r#type = type_from_ref_node_with_env( + RefNode::DataTypeOrImplicit(&variable.nodes.3), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| { + type_alias_from_ref_node( + RefNode::DataTypeOrImplicit(&variable.nodes.3), + syntax_tree, + type_aliases, + ) + }); + let r#type = match (&variable.nodes.3, r#type) { + (_, Some(r#type)) => r#type, + (sv_parser::DataTypeOrImplicit::ImplicitDataType(_), None) => Type::implicit(), + (sv_parser::DataTypeOrImplicit::DataType(_), None) => { + return Err(AnalyzerError::Unsupported( + "unsupported internal data type".to_string(), + )); + } + }; + let r#type = type_with_fallback_ranges_with_env( + r#type, + RefNode::DataTypeOrImplicit(&variable.nodes.3), + syntax_tree, + const_env, + type_aliases, + ); + let mut signals = Vec::new(); + for assignment in variable.nodes.4.nodes.0.contents() { + let sv_parser::VariableDeclAssignment::Variable(assignment) = assignment else { + continue; + }; + let name = identifier_text( + RefNode::VariableIdentifier(&assignment.nodes.0), + syntax_tree, + ) + .ok_or_else(|| AnalyzerError::Unsupported("unsupported signal identifier".to_string()))?; + let signal_type = type_with_unpacked_ranges( + r#type.clone(), + unpacked_ranges_from_variable_dimensions_with_env( + &assignment.nodes.1, + syntax_tree, + const_env, + type_aliases, + )?, + ); + signals.push(Signal::new(name, signal_type)); } + Ok(signals) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Port { - name: String, - direction: PortDirection, - r#type: Type, - is_net: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Signal { - name: String, - r#type: Type, - is_net: bool, -} - -impl Signal { - fn new(name: String, r#type: Type) -> Self { - Self { - name, - r#type, - is_net: false, +fn type_alias_from_ref_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, +) -> Option { + // A cast within a built-in type's range does not make the declared type + // an alias. Inspect the outer type before searching wrapper nodes. + match &node { + RefNode::DataType(data_type) => { + return type_alias_from_data_type(data_type, syntax_tree, type_aliases); } - } - - fn new_net(name: String, r#type: Type) -> Self { - Self { - name, - r#type, - is_net: true, + RefNode::DataTypeOrImplicit(data_type) => { + return type_alias_from_data_type_or_implicit(data_type, syntax_tree, type_aliases); } + _ => {} } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn r#type(&self) -> &Type { - &self.r#type + if let Some(RefNode::DataType(data_type)) = unwrap_node!(node.clone(), DataType) + && let Some(r#type) = type_alias_from_data_type(data_type, syntax_tree, type_aliases) + { + return Some(r#type); } - - pub(crate) fn is_net(&self) -> bool { - self.is_net + if let Some(RefNode::DataTypeOrImplicit(data_type)) = + unwrap_node!(node.clone(), DataTypeOrImplicit) + && let Some(r#type) = + type_alias_from_data_type_or_implicit(data_type, syntax_tree, type_aliases) + { + return Some(r#type); } + let name = + if let Some(RefNode::DataTypeType(data_type)) = unwrap_node!(node.clone(), DataTypeType) { + identifier_text(RefNode::TypeIdentifier(&data_type.nodes.1), syntax_tree)? + } else { + let RefNode::TypeIdentifier(identifier) = unwrap_node!(node, TypeIdentifier)? else { + return None; + }; + identifier_text(RefNode::TypeIdentifier(identifier), syntax_tree)? + }; + type_aliases.get(&name).cloned() } -impl Port { - fn new(name: String, direction: PortDirection, r#type: Type, is_net: bool) -> Self { - Self { - name, - direction, - r#type, - is_net, +fn parameters_from_ref_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + parameters: &mut Vec, + is_local: bool, + base_const_env: &HashMap, + type_aliases: &HashMap, + parameter_overrides: &HashMap, +) -> Result<(), AnalyzerError> { + // Restrict declaration-type queries to the header. Walking the complete + // declaration also visits data types and ranges nested in initializers, + // including the target of a size-function cast. + let type_node = match node.clone() { + RefNode::ParameterDeclaration(sv_parser::ParameterDeclaration::Param(declaration)) => { + RefNode::DataTypeOrImplicit(&declaration.nodes.1) + } + RefNode::LocalParameterDeclaration(sv_parser::LocalParameterDeclaration::Param( + declaration, + )) => RefNode::DataTypeOrImplicit(&declaration.nodes.1), + _ => node.clone(), + }; + if type_node.clone().into_iter().any(|child| { + matches!( + child, + RefNode::DataTypeOrImplicit(sv_parser::DataTypeOrImplicit::DataType(data_type)) + if !matches!( + &**data_type, + sv_parser::DataType::Vector(_) + | sv_parser::DataType::Atom(_) + | sv_parser::DataType::Type(_) + | sv_parser::DataType::ClassType(_) + ) + ) + }) { + return Err(AnalyzerError::Unsupported( + "unsupported parameter data type".to_string(), + )); + } + let declared_alias = type_alias_from_ref_node(type_node.clone(), syntax_tree, type_aliases); + let parameter_width = parameter_declared_width( + type_node.clone(), + syntax_tree, + base_const_env, + parameters, + type_aliases, + parameter_overrides, + ); + let has_declared_type = type_node.clone().into_iter().any(|child| { + matches!( + child, + RefNode::DataTypeOrImplicit(sv_parser::DataTypeOrImplicit::DataType(_)) + ) + }); + let parameter_signed = parameter_width.map(|_| { + declared_alias + .as_ref() + .map(Type::is_signed) + .unwrap_or_else(|| { + integer_atom_expr_type(type_node.clone()) + .map(|r#type| r#type.signed) + .unwrap_or_else(|| is_signed_from_ref_node(type_node.clone()).unwrap_or(false)) + }) + }); + let parameter_is_2state = declared_alias + .or_else(|| type_from_ref_node(type_node, syntax_tree)) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); + for child in node { + if let RefNode::ParamAssignment(param) = child { + let name = parameter_name(RefNode::ParameterIdentifier(¶m.nodes.0), syntax_tree)?; + let mut const_env = base_const_env.clone(); + const_env.extend(const_env_from_parameters(parameters)); + let mut value = param.nodes.2.as_ref().and_then(|(_, expr)| { + const_expr_from_constant_param_with_env(expr, syntax_tree, &const_env, type_aliases) + }); + value = + normalize_unbased_unsized_parameter_value(value, parameter_width, parameter_signed); + // Apply overrides as each declaration is collected so later + // parameter initializers (including casts) are evaluated from the + // specialized values rather than defaults that will be replaced + // only after collection has finished. + if !is_local && let Some(override_value) = parameter_overrides.get(&name) { + value = Some(override_value.clone()); + } + parameters.push(Parameter::new( + name, + value, + parameter_width, + parameter_signed, + parameter_is_2state, + has_declared_type, + is_local, + )); } } + Ok(()) +} - pub fn name(&self) -> &str { - &self.name +fn parameter_declared_width( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + base_const_env: &HashMap, + parameters: &[Parameter], + type_aliases: &HashMap, + parameter_overrides: &HashMap, +) -> Option { + let declared_alias = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases); + let mut range_env = base_const_env.clone(); + range_env.extend(const_env_from_parameters(parameters)); + // Numeric-size casts in a later parameter declaration can refer to an + // earlier assignment in the same parameter-port list. Seed range lowering + // from those assignments while retaining the separate environment below + // for evaluating the resulting symbolic ranges without stale self-values. + for child in node.clone() { + let RefNode::ParamAssignment(parameter) = child else { + continue; + }; + let Ok(name) = parameter_name( + RefNode::ParameterIdentifier(¶meter.nodes.0), + syntax_tree, + ) else { + continue; + }; + let value = parameter_overrides + .get(&name) + .cloned() + .or_else(|| { + parameter.nodes.2.as_ref().and_then(|(_, expr)| { + const_expr_from_constant_param_with_env( + expr, + syntax_tree, + &range_env, + type_aliases, + ) + }) + }) + .and_then(|value| eval_ast_const_expr(&value, &range_env)); + if let Some(value) = value { + range_env.insert(name, value); + } } - - pub fn direction(&self) -> PortDirection { - self.direction + let mut env = base_const_env.clone(); + // A second lowering pass receives values from the first pass in the base + // environment. Do not let stale values for parameters declared by this + // same syntax node resolve its declaration ranges; only constants from + // outside the declaration (such as enum members) belong here. + for child in node.clone() { + if let RefNode::ParamAssignment(parameter) = child + && let Ok(name) = parameter_name( + RefNode::ParameterIdentifier(¶meter.nodes.0), + syntax_tree, + ) + { + env.remove(&name); + } } - - pub fn r#type(&self) -> &Type { - &self.r#type + env.extend(const_env_from_parameters(parameters)); + let mut ranges = + packed_ranges_from_ref_node_with_env(node.clone(), syntax_tree, &range_env, type_aliases); + if let Some(alias) = &declared_alias { + // Use-site dimensions enclose the aliased packed type, just as they + // do for ports, signals, and function types. + ranges.extend(alias.packed_ranges.iter().cloned()); } - - pub fn is_net(&self) -> bool { - self.is_net + if ranges.is_empty() { + if declared_alias.is_some() { + return Some(1); + } + if let Some(r#type) = integer_atom_expr_type(node.clone()) { + return Some(r#type.width); + } + return unwrap_node!(node, IntegerVectorType).is_some().then_some(1); } + ranges.iter().try_fold(1usize, |acc, range| { + let left = eval_ast_const_expr(range.left(), &env)?; + let right = eval_ast_const_expr(range.right(), &env)?; + acc.checked_mul(left.abs_diff(right) as usize + 1) + }) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Instance { - module_name: String, - name: String, - parameter_names: Vec, - parameter_overrides: Vec, - condition: Option, - port_names: Vec, - port_connections: Vec, -} - -impl Instance { - fn new( - module_name: String, - name: String, - parameter_names: Vec, - parameter_overrides: Vec, - condition: Option, - port_names: Vec, - port_connections: Vec, - ) -> Self { - Self { - module_name, - name, - parameter_names, - parameter_overrides, - condition, - port_names, - port_connections, +fn normalize_unbased_unsized_parameter_value( + value: Option, + width: Option, + signed: Option, +) -> Option { + let signing = if signed.unwrap_or(false) { "s" } else { "" }; + match (value, width) { + (Some(ConstExpr::Literal(value)), Some(width)) if value == "'1" => { + let literal = if width <= 128 { + let bits = if width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + }; + format!("{width}'{signing}d{bits}") + } else { + format!("{width}'{signing}b{}", "1".repeat(width)) + }; + Some(ConstExpr::Literal(literal)) + } + (Some(ConstExpr::Literal(value)), Some(width)) if value == "'0" => { + Some(ConstExpr::Literal(format!("{width}'{signing}d0"))) + } + (Some(ConstExpr::Literal(value)), Some(width)) + if matches!(value.as_str(), "'x" | "'X" | "'z" | "'Z" | "'?") => + { + let fill = value.chars().nth(1)?; + Some(ConstExpr::Literal(format!("{width}'{signing}b{fill}"))) } + (value, _) => value, } +} - pub fn module_name(&self) -> &str { - &self.module_name - } +fn const_env_from_parameters(parameters: &[Parameter]) -> HashMap { + let mut env = HashMap::default(); + extend_const_env_with_parameters(&mut env, parameters); + env +} - pub fn name(&self) -> &str { - &self.name +fn extend_const_env_with_parameters(env: &mut HashMap, parameters: &[Parameter]) { + let mut parameter_types = parameter_types_from_const_env(env); + for parameter in parameters { + let Some(value) = parameter.resolved_value(env, ¶meter_types) else { + continue; + }; + if let Some(r#type) = parameter.resolved_type(¶meter_types) { + parameter_types.insert(parameter.name().to_string(), r#type); + insert_parameter_type_markers(env, parameter.name(), r#type); + } + env.insert(parameter.name().to_string(), value); + env.insert(parameter_marker(parameter.name()), value); + if parameter.is_local { + env.insert(local_parameter_marker(parameter.name()), value); + } } +} - pub fn parameter_names(&self) -> &[String] { - &self.parameter_names +fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 { + if width >= 128 { + return value; } - - pub fn parameter_overrides(&self) -> &[ParameterOverride] { - &self.parameter_overrides + if width == 0 { + return 0; } - - pub fn condition(&self) -> Option<&ConstExpr> { - self.condition.as_ref() + let mask = (1u128 << width) - 1; + let bits = (value as u128) & mask; + if signed && bits & (1u128 << (width - 1)) != 0 { + (bits | !mask) as i128 + } else { + bits as i128 } +} - pub fn port_names(&self) -> &[String] { - &self.port_names +fn enum_initializer_fits_base_type(value: i128, value_type: ExprType, base_type: ExprType) -> bool { + // A same-width initializer may change its numeric interpretation when + // the enum base has different signedness, but it does not lose any bits. + if value_type.width <= base_type.width || base_type.width >= 128 { + return true; } - - pub fn port_connections(&self) -> &[PortConnection] { - &self.port_connections + if base_type.width == 0 { + return false; } + if base_type.signed { + let magnitude = 1i128 << (base_type.width - 1); + (-magnitude..magnitude).contains(&value) + } else if value < 0 { + false + } else if base_type.width >= 127 { + true + } else { + value < (1i128 << base_type.width) + } +} +/// Enum member constants collected from module-level `typedef enum` +/// declarations. +#[derive(Default)] +struct EnumMemberConstants { + /// Evaluated values for the module constant environment. + numbers: HashMap, + /// Resolved literal expressions for process-expression substitution. + exprs: HashMap, + /// Base width and signedness retained for early constant substitution. + types: HashMap, +} + +/// Collect enum member constants declared by module-level `typedef enum` +/// declarations. Members must carry explicit values, matching what Veryl +/// emits; each initializer may reference previously declared members of the +/// same module scope. +fn enum_member_constants_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + base_const_env: &HashMap, + type_aliases: &HashMap, + parameter_overrides: &HashMap, +) -> Result { + let mut constants = EnumMemberConstants::default(); + let mut eval_env = base_const_env.clone(); + let mut resolved_type_aliases = type_aliases.clone(); + for item in module_non_port_items(node.clone()) { + let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { + continue; + }; + let data = match declaration { + sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) => { + let mut parameters = Vec::new(); + parameters_from_ref_node( + RefNode::LocalParameterDeclaration(&localparam.0), + syntax_tree, + &mut parameters, + true, + &eval_env, + &resolved_type_aliases, + &HashMap::default(), + )?; + extend_const_env_with_parameters(&mut eval_env, ¶meters); + resolved_type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &eval_env)?; + continue; + } + sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { + let mut parameters = Vec::new(); + parameters_from_ref_node( + RefNode::ParameterDeclaration(¶meter.0), + syntax_tree, + &mut parameters, + false, + &eval_env, + &resolved_type_aliases, + parameter_overrides, + )?; + extend_const_env_with_parameters(&mut eval_env, ¶meters); + resolved_type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &eval_env)?; + continue; + } + sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) => data, + _ => continue, + }; + let sv_parser::DataDeclaration::TypeDeclaration(type_declaration) = &**data else { + continue; + }; + let sv_parser::TypeDeclaration::DataType(type_declaration) = &**type_declaration else { + continue; + }; + let sv_parser::DataType::Enum(r#enum) = &type_declaration.nodes.1 else { + continue; + }; + let member_type = match &r#enum.nodes.1 { + Some(base) => type_from_ref_node_with_env( + RefNode::EnumBaseType(base), + syntax_tree, + &eval_env, + &resolved_type_aliases, + ) + .or_else(|| { + type_alias_from_ref_node( + RefNode::EnumBaseType(base), + syntax_tree, + &resolved_type_aliases, + ) + }) + .and_then(|r#type| expr_type_from_type(&r#type, &eval_env)), + None => Some(ExprType { + width: 32, + signed: true, + }), + } + .ok_or_else(|| AnalyzerError::Unsupported("enum base type".to_string()))?; + for member in r#enum.nodes.2.nodes.1.contents() { + let name = identifier_text(RefNode::Identifier(&member.nodes.0.nodes.0), syntax_tree) + .ok_or_else(|| { + AnalyzerError::Unsupported("enum member identifier".to_string()) + })?; + if member.nodes.1.is_some() { + return Err(AnalyzerError::Unsupported(format!( + "ranged enum member `{name}`" + ))); + } + let Some((_, value)) = &member.nodes.2 else { + return Err(AnalyzerError::Unsupported(format!( + "enum member `{name}` without an explicit value" + ))); + }; + let value = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(value), + syntax_tree, + &eval_env, + &resolved_type_aliases, + ) + .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` value")))?; + let value = match value { + ConstExpr::Literal(literal) => ConstExpr::Literal( + resize_unbased_fill_literal_for_cast( + &literal, + member_type.width, + member_type.signed, + ) + .unwrap_or(literal), + ), + value => value, + }; + let value_type = + infer_const_expr_type(&value, ¶meter_types_from_const_env(&eval_env)) + .ok_or_else(|| { + AnalyzerError::Unsupported(format!("enum member `{name}` value type")) + })?; + let number = eval_ast_const_expr(&value, &eval_env).ok_or_else(|| { + AnalyzerError::Unsupported(format!("unresolved enum member `{name}` value")) + })?; + if !enum_initializer_fits_base_type(number, value_type, member_type) { + return Err(AnalyzerError::Unsupported(format!( + "enum member `{name}` value does not fit its base type" + ))); + } + let number = + coerce_const_parameter_value(number, member_type.width, member_type.signed); + constants.numbers.insert(name.clone(), number); + eval_env.insert(name.clone(), number); + insert_parameter_type_markers(&mut eval_env, &name, member_type); + constants.types.insert(name.clone(), member_type); + constants.exprs.insert( + name, + Expr::Literal(format_typed_parameter_literal( + number, + member_type.width, + member_type.signed, + )), + ); + } + // A later typedef may use a cast or range that depends on this + // enum's members. Rebuild aliases from the enriched environment + // before resolving a subsequent enum base through that typedef. + resolved_type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, &eval_env)?; + } + Ok(constants) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParameterOverride { - name: String, - value: Option, -} - -impl ParameterOverride { - fn new(name: String, value: Option) -> Self { - Self { name, value } - } +fn parameter_value_env( + parameters: &[Parameter], + const_env: &HashMap, +) -> HashMap { + let mut values = HashMap::default(); + let mut parameter_types = HashMap::default(); + for parameter in parameters { + let inferred_type = parameter.value().and_then(|value| { + infer_parameter_value_type(value, parameter.has_declared_type, ¶meter_types) + }); + let width = parameter + .declared_width + .or(inferred_type.map(|r#type| r#type.width)); + let signed = parameter + .declared_signed + .or(inferred_type.map(|r#type| r#type.signed)) + .unwrap_or(false); + if let Some(width) = width { + parameter_types.insert(parameter.name().to_string(), ExprType { width, signed }); + } - pub fn name(&self) -> &str { - &self.name + let mut value = if let Some(value) = const_env.get(parameter.name()).copied() { + if let Some(width) = width { + Expr::Literal(format_typed_parameter_literal(value, width, signed)) + } else if value.is_negative() { + let width = (128 - (!value as u128).leading_zeros() as usize + 1).max(32); + let mask = if width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + }; + Expr::Literal(format!("{width}'sd{}", (value as u128) & mask)) + } else if let Some(ConstExpr::Literal(literal)) = parameter.value() { + Expr::Literal(literal.clone()) + } else { + Expr::Literal(value.to_string()) + } + } else if let Some(value) = parameter.value().cloned() { + let value = substitute_typed_parameter_literals(value, const_env, ¶meter_types); + let value = replace_oob_const_selects_with_unknown(value, const_env); + let value = substitute_expr_constants_with_parameter_literals( + const_expr_to_expr(value), + const_env, + &values, + ); + if let Some(width) = width { + match value { + Expr::Literal(literal) => { + let resized = resize_unbased_fill_literal_for_cast(&literal, width, signed) + .or_else(|| { + typecheck::parse_integral_literal(&literal).map(|literal| { + resize_integral_literal_for_cast(literal, width, signed) + }) + }); + resized.map_or_else( + || Expr::Resize { + expr: Box::new(Expr::Literal(literal)), + width, + signed, + }, + Expr::Literal, + ) + } + value => Expr::Resize { + expr: Box::new(value), + width, + signed, + }, + } + } else { + value + } + } else { + continue; + }; + if parameter.declared_is_2state { + value = Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(value), + }; + } + values.insert(parameter.name().to_string(), value); } + values +} - pub fn value(&self) -> Option<&ConstExpr> { - self.value.as_ref() +fn replace_oob_const_selects_with_unknown( + expr: ConstExpr, + const_env: &HashMap, +) -> ConstExpr { + match expr { + ConstExpr::Select { expr, bit } => { + let expr = replace_oob_const_selects_with_unknown(*expr, const_env); + let bit = replace_oob_const_selects_with_unknown(*bit, const_env); + if let ConstExpr::Literal(literal) = &expr + && let Some(width) = + typecheck::parse_integral_literal(literal).map(|literal| literal.width) + && match typecheck::eval_const_expr(&bit.clone().into(), const_env) { + Some(bit_index) => { + usize::try_from(bit_index).map_or(true, |bit_index| bit_index >= width) + } + None => const_expr_contains_unknown_literal(&bit), + } + { + ConstExpr::Literal("1'bx".to_string()) + } else { + ConstExpr::Select { + expr: Box::new(expr), + bit: Box::new(bit), + } + } + } + ConstExpr::Function { name, args } => ConstExpr::Function { + name, + args: args + .into_iter() + .map(|arg| replace_oob_const_selects_with_unknown(arg, const_env)) + .collect(), + }, + ConstExpr::Unary { op, expr } => ConstExpr::Unary { + op, + expr: Box::new(replace_oob_const_selects_with_unknown(*expr, const_env)), + }, + ConstExpr::Binary { left, op, right } => ConstExpr::Binary { + left: Box::new(replace_oob_const_selects_with_unknown(*left, const_env)), + op, + right: Box::new(replace_oob_const_selects_with_unknown(*right, const_env)), + }, + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => ConstExpr::Mux { + condition: Box::new(replace_oob_const_selects_with_unknown( + *condition, const_env, + )), + then_expr: Box::new(replace_oob_const_selects_with_unknown( + *then_expr, const_env, + )), + else_expr: Box::new(replace_oob_const_selects_with_unknown( + *else_expr, const_env, + )), + }, + ConstExpr::Ident(name) => ConstExpr::Ident(name), + ConstExpr::Literal(value) => ConstExpr::Literal(value), } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PortConnection { - formal: String, - actual: String, - actual_expr: Option, -} - -impl PortConnection { - fn new(formal: String, actual: String, actual_expr: Option) -> Self { - Self { - formal, - actual, - actual_expr, +fn const_expr_contains_unknown_literal(expr: &ConstExpr) -> bool { + match expr { + ConstExpr::Literal(literal) => typecheck::parse_integral_literal(literal) + .is_some_and(|literal| literal.mask != Default::default()), + ConstExpr::Ident(_) => false, + ConstExpr::Select { expr, bit } => { + const_expr_contains_unknown_literal(expr) || const_expr_contains_unknown_literal(bit) + } + ConstExpr::Function { args, .. } => args.iter().any(const_expr_contains_unknown_literal), + ConstExpr::Unary { expr, .. } => const_expr_contains_unknown_literal(expr), + ConstExpr::Binary { left, right, .. } => { + const_expr_contains_unknown_literal(left) || const_expr_contains_unknown_literal(right) + } + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => { + const_expr_contains_unknown_literal(condition) + || const_expr_contains_unknown_literal(then_expr) + || const_expr_contains_unknown_literal(else_expr) } } +} - pub fn formal(&self) -> &str { - &self.formal - } - - pub fn actual(&self) -> &str { - &self.actual +fn const_expr_to_expr(expr: ConstExpr) -> Expr { + match expr { + ConstExpr::Literal(value) => Expr::Literal(value), + ConstExpr::Ident(name) => Expr::Ident(name), + ConstExpr::Select { expr, bit } => Expr::Select { + expr: Box::new(const_expr_to_expr(*expr)), + msb: (*bit).clone(), + lsb: *bit, + signed: false, + }, + ConstExpr::Function { name, args } => Expr::Call { + name, + args: args.into_iter().map(const_expr_to_expr).collect(), + }, + ConstExpr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(const_expr_to_expr(*expr)), + }, + ConstExpr::Binary { left, op, right } => Expr::Binary { + left: Box::new(const_expr_to_expr(*left)), + op, + right: Box::new(const_expr_to_expr(*right)), + }, + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(const_expr_to_expr(*condition)), + then_expr: Box::new(const_expr_to_expr(*then_expr)), + else_expr: Box::new(const_expr_to_expr(*else_expr)), + }, } +} - pub fn actual_expr(&self) -> Option<&Expr> { - self.actual_expr.as_ref() +fn format_typed_parameter_literal(value: i128, width: usize, signed: bool) -> String { + let signing = if signed { "s" } else { "" }; + if width <= 128 { + let mask = if width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + }; + let bits = (value as u128) & mask; + format!("{width}'{signing}d{bits}") + } else { + let extension = if value.is_negative() { '1' } else { '0' }; + let high_bits = extension.to_string().repeat(width - 128); + let low_bits = value as u128; + format!("{width}'{signing}b{high_bits}{low_bits:0128b}") } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PortDirection { - Input, - Output, - Inout, - Ref, - Unspecified, +#[derive(Clone, Copy)] +pub(crate) struct ExprType { + pub(crate) width: usize, + pub(crate) signed: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Type { - kind: TypeKind, - is_signed: bool, - packed_ranges: Vec, - unpacked_ranges: Vec, +fn substitute_typed_parameter_literals( + expr: ConstExpr, + constants: &HashMap, + parameter_types: &HashMap, +) -> ConstExpr { + match expr { + ConstExpr::Ident(name) => match (constants.get(&name), parameter_types.get(&name)) { + (Some(value), Some(r#type)) => ConstExpr::Literal(format_typed_parameter_literal( + *value, + r#type.width, + r#type.signed, + )), + _ => ConstExpr::Ident(name), + }, + ConstExpr::Literal(value) => ConstExpr::Literal(value), + ConstExpr::Select { expr, bit } => ConstExpr::Select { + expr: Box::new(substitute_typed_parameter_literals( + *expr, + constants, + parameter_types, + )), + bit: Box::new(substitute_typed_parameter_literals( + *bit, + constants, + parameter_types, + )), + }, + ConstExpr::Function { name, args } => ConstExpr::Function { + name, + args: args + .into_iter() + .map(|arg| substitute_typed_parameter_literals(arg, constants, parameter_types)) + .collect(), + }, + ConstExpr::Unary { op, expr } => ConstExpr::Unary { + op, + expr: Box::new(substitute_typed_parameter_literals( + *expr, + constants, + parameter_types, + )), + }, + ConstExpr::Binary { left, op, right } => ConstExpr::Binary { + left: Box::new(substitute_typed_parameter_literals( + *left, + constants, + parameter_types, + )), + op, + right: Box::new(substitute_typed_parameter_literals( + *right, + constants, + parameter_types, + )), + }, + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => ConstExpr::Mux { + condition: Box::new(substitute_typed_parameter_literals( + *condition, + constants, + parameter_types, + )), + then_expr: Box::new(substitute_typed_parameter_literals( + *then_expr, + constants, + parameter_types, + )), + else_expr: Box::new(substitute_typed_parameter_literals( + *else_expr, + constants, + parameter_types, + )), + }, + } } -impl Type { - fn implicit() -> Self { - Self { - kind: TypeKind::Implicit, - is_signed: false, - packed_ranges: Vec::new(), - unpacked_ranges: Vec::new(), +fn infer_const_expr_type( + expr: &ConstExpr, + parameter_types: &HashMap, +) -> Option { + match expr { + ConstExpr::Literal(literal) => { + let literal = typecheck::parse_integral_literal(literal)?; + Some(ExprType { + width: literal.width, + signed: literal.signed, + }) + } + ConstExpr::Ident(name) => parameter_types.get(name).copied(), + ConstExpr::Select { .. } => Some(ExprType { + width: 1, + signed: false, + }), + ConstExpr::Function { name, .. } => match name.as_str() { + "$clog2" => Some(ExprType { + width: 32, + signed: true, + }), + "$onehot" | "$onehot0" => Some(ExprType { + width: 1, + signed: false, + }), + _ => None, + }, + ConstExpr::Unary { op, expr } => { + let operand = infer_const_expr_type(expr, parameter_types)?; + if matches!( + op, + UnaryOp::LogicNot | UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor + ) { + Some(ExprType { + width: 1, + signed: false, + }) + } else { + Some(operand) + } + } + ConstExpr::Binary { left, op, right } => { + let left = infer_const_expr_type(left, parameter_types)?; + let right = infer_const_expr_type(right, parameter_types)?; + match op { + BinaryOp::LogicAnd + | BinaryOp::LogicOr + | BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::EqCase + | BinaryOp::NeCase + | BinaryOp::EqWildcard + | BinaryOp::NeWildcard + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge => Some(ExprType { + width: 1, + signed: false, + }), + BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar => Some(left), + _ => Some(ExprType { + width: left.width.max(right.width), + signed: left.signed && right.signed, + }), + } } - } - - fn new(kind: TypeKind) -> Self { - Self { - kind, - is_signed: false, - packed_ranges: Vec::new(), - unpacked_ranges: Vec::new(), + ConstExpr::Mux { + then_expr, + else_expr, + .. + } => { + let then_type = infer_const_expr_type(then_expr, parameter_types)?; + let else_type = infer_const_expr_type(else_expr, parameter_types)?; + Some(ExprType { + width: then_type.width.max(else_type.width), + signed: then_type.signed && else_type.signed, + }) } } - - pub fn kind(&self) -> TypeKind { - self.kind - } - - pub fn is_signed(&self) -> bool { - self.is_signed - } - - pub fn packed_ranges(&self) -> &[PackedRange] { - &self.packed_ranges - } - - pub fn unpacked_ranges(&self) -> &[UnpackedRange] { - &self.unpacked_ranges - } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TypeKind { - Bit, - Logic, - Reg, - Implicit, +fn infer_parameter_value_type( + value: &ConstExpr, + has_declared_type: bool, + parameter_types: &HashMap, +) -> Option { + if !has_declared_type + && let ConstExpr::Literal(literal) = value + && matches!( + literal.trim(), + "'0" | "'1" | "'x" | "'X" | "'z" | "'Z" | "'?" + ) + { + Some(ExprType { + width: 1, + signed: false, + }) + } else { + infer_const_expr_type(value, parameter_types) + } } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PackedRange { +struct PackedDimension { left: ConstExpr, right: ConstExpr, -} - -impl PackedRange { - fn new(left: ConstExpr, right: ConstExpr) -> Self { - Self { left, right } - } - - pub fn left(&self) -> &ConstExpr { - &self.left - } - - pub fn right(&self) -> &ConstExpr { - &self.right - } + width: ConstExpr, + normalize_single: bool, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct UnpackedRange { +struct UnpackedDimension { left: ConstExpr, right: ConstExpr, - size: Option, -} - -impl UnpackedRange { - fn new(left: ConstExpr, right: ConstExpr) -> Self { - Self { - left, - right, - size: None, - } - } - - fn sized(size: ConstExpr) -> Self { - Self { - left: ConstExpr::Literal("0".to_string()), - right: ConstExpr::Binary { - left: Box::new(size.clone()), - op: BinaryOp::Sub, - right: Box::new(ConstExpr::Literal("1".to_string())), - }, - size: Some(size), - } - } - - pub fn left(&self) -> &ConstExpr { - &self.left - } - - pub fn right(&self) -> &ConstExpr { - &self.right - } - - fn size(&self) -> Option<&ConstExpr> { - self.size.as_ref() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ConstExpr { - Literal(String), - Ident(String), - Select { - expr: Box, - bit: Box, - }, - Function { - name: String, - args: Vec, - }, - Unary { - op: UnaryOp, - expr: Box, - }, - Binary { - left: Box, - op: BinaryOp, - right: Box, - }, - Mux { - condition: Box, - then_expr: Box, - else_expr: Box, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UnaryOp { - Plus, - Minus, - BitNot, - LogicNot, - ToTwoState, - RedAnd, - RedOr, - RedXor, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BinaryOp { - Add, - Sub, - Mul, - Div, - Mod, - Shl, - Shr, - Sar, - BitAnd, - BitOr, - BitXor, - LogicAnd, - LogicOr, - Eq, - Ne, - EqCase, - NeCase, - EqWildcard, - NeWildcard, - Lt, - Le, - Gt, - Ge, + width: ConstExpr, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Assignment { - lhs: LValue, - rhs: Expr, -} - -impl Assignment { - fn new(lhs: LValue, rhs: Expr) -> Self { - Self { lhs, rhs } - } - - pub fn lhs(&self) -> &str { - self.lhs.name() - } - - pub fn lhs_value(&self) -> &LValue { - &self.lhs - } - - pub fn rhs(&self) -> &Expr { - &self.rhs - } +struct VariableDimensions { + packed: Vec, + unpacked: Vec, + signed: bool, + is_2state: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LValue { - Ident(String), - Select { - name: String, - msb: ConstExpr, - lsb: ConstExpr, - signed: bool, - array_slice_width: Option, - array_slice_reversed: bool, - }, -} +type VariablePackedDimensions = HashMap; -impl LValue { - pub fn name(&self) -> &str { - match self { - LValue::Ident(name) | LValue::Select { name, .. } => name, - } - } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct FunctionReturnMetadata { + width: Option, + first_packed_dimension_width: Option, + signed: bool, + is_2state: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CombProcess { - kind: CombProcessKind, - condition: Option, - assignments: Vec, +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct PackedDimensions { + variables: VariablePackedDimensions, + const_env: HashMap, + type_aliases: HashMap, + function_return_types: HashMap, + functions: Arc>, + expression_signedness: Arc>, } -impl CombProcess { +impl PackedDimensions { fn new( - kind: CombProcessKind, - condition: Option, - assignments: Vec, + variables: VariablePackedDimensions, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Self { - Self { - kind, - condition, - assignments, + Self { + variables, + const_env: const_env.clone(), + type_aliases: type_aliases.clone(), + function_return_types: HashMap::default(), + functions: Arc::default(), + expression_signedness: Arc::default(), } } +} - pub fn kind(&self) -> CombProcessKind { - self.kind +impl Deref for PackedDimensions { + type Target = VariablePackedDimensions; + + fn deref(&self) -> &Self::Target { + &self.variables } +} - pub fn condition(&self) -> Option<&ConstExpr> { - self.condition.as_ref() +impl DerefMut for PackedDimensions { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.variables } +} - pub fn assignments(&self) -> &[Assignment] { - &self.assignments +fn packed_dimensions_from_ports_and_signals( + ports: &[Port], + signals: &[Signal], + const_env: &HashMap, + type_aliases: &HashMap, +) -> PackedDimensions { + let mut dimensions = HashMap::default(); + for port in ports { + dimensions.insert( + port.name().to_string(), + VariableDimensions { + packed: packed_dimension_widths(port.r#type().packed_ranges()), + unpacked: unpacked_dimension_widths(port.r#type().unpacked_ranges()), + signed: port.r#type().is_signed(), + is_2state: port.r#type().kind() == TypeKind::Bit, + }, + ); + } + for signal in signals { + dimensions.insert( + signal.name().to_string(), + VariableDimensions { + packed: packed_dimension_widths(signal.r#type().packed_ranges()), + unpacked: unpacked_dimension_widths(signal.r#type().unpacked_ranges()), + signed: signal.r#type().is_signed(), + is_2state: signal.r#type().kind() == TypeKind::Bit, + }, + ); } + PackedDimensions::new(dimensions, const_env, type_aliases) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CombProcessKind { - ContinuousAssign, - AlwaysComb, +fn packed_dimension_widths(ranges: &[PackedRange]) -> Vec { + ranges + .iter() + .map(|range| { + let left = range.left().clone(); + let right = range.right().clone(); + let width = |high: ConstExpr, low: ConstExpr| ConstExpr::Binary { + left: Box::new(ConstExpr::Binary { + left: Box::new(high), + op: BinaryOp::Sub, + right: Box::new(low), + }), + op: BinaryOp::Add, + right: Box::new(ConstExpr::Literal("1".to_string())), + }; + let width = ConstExpr::Mux { + condition: Box::new(ConstExpr::Binary { + left: Box::new(left.clone()), + op: BinaryOp::Ge, + right: Box::new(right.clone()), + }), + then_expr: Box::new(width(left.clone(), right.clone())), + else_expr: Box::new(width(right.clone(), left.clone())), + }; + PackedDimension { + left, + right, + width, + normalize_single: false, + } + }) + .collect() } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FfProcess { - events: Vec, - assignments: Vec, +fn unpacked_dimension_widths(ranges: &[UnpackedRange]) -> Vec { + ranges + .iter() + .map(|range| { + let left = range.left().clone(); + let right = range.right().clone(); + let width = ConstExpr::Mux { + condition: Box::new(ConstExpr::Binary { + left: Box::new(left.clone()), + op: BinaryOp::Ge, + right: Box::new(right.clone()), + }), + then_expr: Box::new(ConstExpr::Binary { + left: Box::new(left.clone()), + op: BinaryOp::Sub, + right: Box::new(right.clone()), + }), + else_expr: Box::new(ConstExpr::Binary { + left: Box::new(right.clone()), + op: BinaryOp::Sub, + right: Box::new(left.clone()), + }), + }; + UnpackedDimension { + left, + right, + width: add_expr(width, ConstExpr::Literal("1".to_string())), + } + }) + .collect() } -impl FfProcess { - fn new(events: Vec, assignments: Vec) -> Self { - Self { - events, - assignments, - } - } +fn function_packed_dimension_widths(ranges: &[PackedRange]) -> Vec { + packed_dimension_widths(ranges) + .into_iter() + .map(|mut dimension| { + dimension.normalize_single = true; + dimension + }) + .collect() +} - pub fn events(&self) -> &[FfEvent] { - &self.events - } +fn function_param_packed_dimensions( + data_type: &sv_parser::DataTypeOrImplicit, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Vec { + function_type_from_ref_node( + RefNode::DataTypeOrImplicit(data_type), + syntax_tree, + const_env, + type_aliases, + ) + .map(|r#type| function_packed_dimension_widths(r#type.packed_ranges())) + .unwrap_or_default() +} - pub fn assignments(&self) -> &[ConditionalAssignment] { - &self.assignments - } +fn parameter_marker(name: &str) -> String { + format!("__parameter::{name}") } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FfEdge { - Pos, - Neg, +fn local_parameter_marker(name: &str) -> String { + format!("__parameter::local::{name}") } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FfEvent { - edge: FfEdge, - signal: String, +fn enum_marker(name: &str) -> String { + format!("__enum::{name}") } -impl FfEvent { - fn new(edge: FfEdge, signal: String) -> Self { - Self { edge, signal } - } +fn parameter_width_marker(name: &str) -> String { + format!("__parameter::width::{name}") +} - pub fn edge(&self) -> FfEdge { - self.edge - } +fn parameter_signed_marker(name: &str) -> String { + format!("__parameter::signed::{name}") +} - pub fn signal(&self) -> &str { - &self.signal - } +fn variable_bits_marker(name: &str) -> String { + format!("__variable::bits::{name}") } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConditionalAssignment { - condition: Option, - assignment: Assignment, +fn variable_size_marker(name: &str) -> String { + format!("__variable::size::{name}") } -impl ConditionalAssignment { - fn new(condition: Option, assignment: Assignment) -> Self { - Self { - condition, - assignment, +fn variable_signed_marker(name: &str) -> String { + format!("__variable::signed::{name}") +} + +fn variable_size_function_width( + const_env: &HashMap, + name: &str, + first_dimension_only: bool, +) -> Option { + let marker = if first_dimension_only { + variable_size_marker(name) + } else { + variable_bits_marker(name) + }; + usize::try_from(*const_env.get(&marker)?).ok() +} + +fn variable_type_is_signed(const_env: &HashMap, name: &str) -> bool { + const_env + .get(&variable_signed_marker(name)) + .is_some_and(|signed| *signed != 0) +} + +fn extend_const_env_with_variable_types<'a>( + const_env: &mut HashMap, + variables: impl Iterator, +) { + for (name, r#type) in variables { + let dimension_width = |range: &PackedRange| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + usize::try_from(left.abs_diff(right)).ok()?.checked_add(1) + }; + let unpacked_widths = r#type + .unpacked_ranges() + .iter() + .map(|range| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + usize::try_from(left.abs_diff(right)).ok()?.checked_add(1) + }) + .collect::>>(); + let packed_widths = r#type + .packed_ranges() + .iter() + .map(dimension_width) + .collect::>>(); + let (Some(unpacked_widths), Some(packed_widths)) = (unpacked_widths, packed_widths) else { + continue; + }; + let bits = unpacked_widths + .iter() + .chain(&packed_widths) + .try_fold(1usize, |width, dimension| width.checked_mul(*dimension)); + let size = unpacked_widths + .first() + .or_else(|| packed_widths.first()) + .copied() + .unwrap_or(1); + if let Some(bits) = bits.and_then(|bits| i128::try_from(bits).ok()) { + const_env.insert(variable_bits_marker(name), bits); + } + if let Ok(size) = i128::try_from(size) { + const_env.insert(variable_size_marker(name), size); } + const_env.insert(variable_signed_marker(name), r#type.is_signed() as i128); } +} - pub fn condition(&self) -> Option<&Expr> { - self.condition.as_ref() +fn insert_parameter_type_markers( + const_env: &mut HashMap, + name: &str, + r#type: ExprType, +) { + if let Ok(width) = i128::try_from(r#type.width) { + const_env.insert(parameter_width_marker(name), width); + const_env.insert(parameter_signed_marker(name), r#type.signed as i128); } +} - pub fn assignment(&self) -> &Assignment { - &self.assignment +fn parameter_types_from_const_env(const_env: &HashMap) -> HashMap { + const PREFIX: &str = "__parameter::width::"; + const_env + .iter() + .filter_map(|(marker, width)| { + let name = marker.strip_prefix(PREFIX)?; + let width = usize::try_from(*width).ok()?; + let signed = const_env + .get(¶meter_signed_marker(name)) + .is_some_and(|signed| *signed != 0); + Some((name.to_string(), ExprType { width, signed })) + }) + .collect() +} + +fn module_parameter_port_list(node: RefNode<'_>) -> Option> { + match node { + RefNode::ModuleDeclarationAnsi(module) => module + .nodes + .0 + .nodes + .5 + .as_ref() + .map(RefNode::ParameterPortList), + RefNode::ModuleDeclarationNonansi(module) => module + .nodes + .0 + .nodes + .5 + .as_ref() + .map(RefNode::ParameterPortList), + _ => None, } } -#[derive(Debug, Clone, PartialEq, Eq)] -struct Function { - name: String, - params: Vec, - body: Expr, - return_width: Option, - return_signed: bool, - return_is_2state: bool, +fn module_non_port_items(node: RefNode<'_>) -> Vec<&sv_parser::NonPortModuleItem> { + match node { + RefNode::ModuleDeclarationAnsi(module) => module.nodes.2.iter().collect(), + RefNode::ModuleDeclarationNonansi(_) => Vec::new(), + _ => Vec::new(), + } } -#[derive(Debug, Clone, PartialEq, Eq)] -struct FunctionParam { - name: String, - width: Option, - signed: bool, - is_2state: bool, - packed_dimensions: Vec, +fn package_or_generate_declaration_from_non_port_item( + item: &sv_parser::NonPortModuleItem, +) -> Option<&sv_parser::PackageOrGenerateItemDeclaration> { + let sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) = item else { + return None; + }; + let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { + return None; + }; + let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 + else { + return None; + }; + let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = + &**declaration + else { + return None; + }; + Some(declaration) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct FunctionLocalType { - width: usize, - signed: bool, - is_2state: bool, +fn parameter_name(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Result { + let locate = identifier_locate(node).ok_or_else(|| { + AnalyzerError::Unsupported("unsupported parameter identifier".to_string()) + })?; + syntax_tree + .get_str(&locate) + .map(str::to_string) + .ok_or_else(|| AnalyzerError::Unsupported("invalid parameter identifier span".to_string())) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Expr { - Ident(String), - Literal(String), - Select { - expr: Box, - msb: ConstExpr, - lsb: ConstExpr, - signed: bool, - }, - Concat(Vec), - RepeatConcat { - count: ConstExpr, - parts: Vec, - }, - Resize { - expr: Box, - width: usize, - signed: bool, - }, - Unary { - op: UnaryOp, - expr: Box, - }, - Binary { - left: Box, - op: BinaryOp, - right: Box, - }, - Mux { - condition: Box, - then_expr: Box, - else_expr: Box, - }, - Call { - name: String, - args: Vec, - }, +fn port_name(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Result { + let locate = identifier_locate(node) + .ok_or_else(|| AnalyzerError::Unsupported("unsupported port identifier".to_string()))?; + syntax_tree + .get_str(&locate) + .map(str::to_string) + .ok_or_else(|| AnalyzerError::Unsupported("invalid port identifier span".to_string())) } -fn identifier_locate(node: RefNode<'_>) -> Option { - match unwrap_node!(node, SimpleIdentifier, EscapedIdentifier) { - Some(RefNode::SimpleIdentifier(identifier)) => Some(identifier.nodes.0), - Some(RefNode::EscapedIdentifier(identifier)) => Some(identifier.nodes.0), - _ => None, +fn instances_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, +) -> Result, AnalyzerError> { + let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; + for child in node.clone() { + let RefNode::ModuleInstantiation(instantiation) = child else { + continue; + }; + let module_name = identifier_text( + RefNode::ModuleIdentifier(&instantiation.nodes.0), + syntax_tree, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) + })?; + if !type_aliases.contains_key(&module_name) + && instantiation + .nodes + .2 + .contents() + .iter() + .any(|instance| !instance.nodes.0.nodes.1.is_empty()) + { + return Err(AnalyzerError::Unsupported( + "module instance array".to_string(), + )); + } + } + let mut instances = Vec::new(); + for item in module_non_port_items(node) { + instances_from_non_port_module_item( + item, + None, + syntax_tree, + const_env, + packed_dimensions, + &mut instances, + )?; } + instances.retain(|instance| !type_aliases.contains_key(instance.module_name())); + Ok(instances) } -fn ports_from_module_node( - node: RefNode<'_>, +fn instances_from_non_port_module_item( + item: &sv_parser::NonPortModuleItem, + condition: Option, syntax_tree: &SyntaxTree, -) -> Result, AnalyzerError> { - let mut ports = Vec::new(); - let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - let mut inherited_direction = PortDirection::Unspecified; - let mut inherited_type = Type::implicit(); - for child in node { - match child { - RefNode::AnsiPortDeclarationNet(port) => { - let header = port.nodes.0.as_ref(); - let direction = header - .and_then(|header| direction_from_ref_node(header.into())) - .unwrap_or(inherited_direction); - let r#type = match header { - Some(header) => type_from_net_port_header(header, syntax_tree, &type_aliases) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported port data type".to_string()) - })?, - None => inherited_type.clone(), - }; - let r#type = type_with_fallback_ranges( - r#type, - RefNode::AnsiPortDeclarationNet(port), - syntax_tree, - &type_aliases, - ); - let inherited_type_base = r#type.clone(); - let r#type = type_with_unpacked_ranges( - r#type, - unpacked_ranges_from_dimensions(&port.nodes.2, syntax_tree)?, - ); - let name = port_name(RefNode::PortIdentifier(&port.nodes.1), syntax_tree)?; - inherited_direction = direction; - inherited_type = inherited_type_base; - ports.push(Port::new(name, direction, r#type, true)); - } - RefNode::AnsiPortDeclarationVariable(port) => { - let header = port.nodes.0.as_ref(); - let direction = header - .and_then(|header| direction_from_ref_node(header.into())) - .unwrap_or(inherited_direction); - let r#type = match header { - Some(header) => { - type_from_variable_port_header(header, syntax_tree, &type_aliases) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported port data type".to_string()) - })? - } - None => inherited_type.clone(), - }; - let r#type = type_with_fallback_ranges( - r#type, - RefNode::AnsiPortDeclarationVariable(port), + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::NonPortModuleItem::GenerateRegion(region) => { + for item in ®ion.nodes.1 { + instances_from_generate_item( + item, + condition.clone(), syntax_tree, - &type_aliases, - ); - let inherited_type_base = r#type.clone(); - let r#type = type_with_unpacked_ranges( - r#type, - unpacked_ranges_from_variable_dimensions(&port.nodes.2, syntax_tree)?, - ); - let name = port_name(RefNode::PortIdentifier(&port.nodes.1), syntax_tree)?; - inherited_direction = direction; - inherited_type = inherited_type_base; - ports.push(Port::new(name, direction, r#type, false)); - } - RefNode::AnsiPortDeclarationParen(port) => { - let explicit_direction = port.nodes.0.as_ref().map(direction_from_port_direction); - let direction = explicit_direction.unwrap_or(inherited_direction); - let r#type = if explicit_direction.is_some() { - Type::implicit() - } else { - inherited_type.clone() - }; - let name = port_name(RefNode::PortIdentifier(&port.nodes.2), syntax_tree)?; - inherited_direction = direction; - inherited_type = r#type.clone(); - ports.push(Port::new(name, direction, r#type, false)); + const_env, + packed_dimensions, + instances, + )?; } - _ => {} } + sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { + instances_from_module_or_generate_item( + item, + condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; + } + _ => {} + } + Ok(()) +} + +fn instances_from_generate_item( + item: &sv_parser::GenerateItem, + condition: Option, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { + instances_from_module_or_generate_item( + item, + condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; } - Ok(ports) + Ok(()) } -fn parameters_from_module_node( - node: RefNode<'_>, +fn instances_from_module_or_generate_item( + item: &sv_parser::ModuleOrGenerateItem, + condition: Option, syntax_tree: &SyntaxTree, -) -> Result, AnalyzerError> { - let mut parameters = Vec::new(); - if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { - parameters_from_ref_node( - parameter_port_list.clone(), - syntax_tree, - &mut parameters, - false, - )?; - let mut local_parameters = Vec::new(); - for child in parameter_port_list { - if let RefNode::LocalParameterDeclaration(localparam) = child { - parameters_from_ref_node( - RefNode::LocalParameterDeclaration(localparam), - syntax_tree, - &mut local_parameters, - true, - )?; - } + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::ModuleOrGenerateItem::Module(module) => { + instances_from_module_instantiation( + &module.nodes.1, + condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; } - for local in local_parameters { - if let Some(parameter) = parameters - .iter_mut() - .find(|parameter| parameter.name == local.name) - { - parameter.is_local = true; - } + sv_parser::ModuleOrGenerateItem::ModuleItem(item) => { + instances_from_module_common_item( + &item.nodes.1, + condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; } + _ => {} } + Ok(()) +} - for item in module_non_port_items(node.clone()) { - if let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) { - match declaration { - sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration( - localparam, - ) => parameters_from_ref_node( - RefNode::LocalParameterDeclaration(&localparam.0), - syntax_tree, - &mut parameters, - true, - )?, - sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { - parameters_from_ref_node( - RefNode::ParameterDeclaration(¶meter.0), - syntax_tree, - &mut parameters, - false, - )? - } - _ => {} - } +fn instances_from_module_common_item( + item: &sv_parser::ModuleCommonItem, + condition: Option, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { + instances_from_conditional_generate( + generate, + condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; } + sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(_) => {} + _ => {} } - - Ok(parameters) + Ok(()) } -fn signals_from_module_node( - node: RefNode<'_>, +fn instances_from_conditional_generate( + generate: &sv_parser::ConditionalGenerateConstruct, + condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, -) -> Result, AnalyzerError> { - let mut signals = Vec::new(); - let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - for item in module_non_port_items(node) { - signals_from_non_port_module_item( - item, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { + return Ok(()); + }; + let Some(generate_condition) = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1), + syntax_tree, + const_env, + &packed_dimensions.type_aliases, + ) else { + return Err(AnalyzerError::Unsupported( + "conditional-generate condition lowering".to_string(), + )); + }; + let generate_condition = substitute_const_expr_constants(generate_condition, const_env); + let then_condition = combine_conditions(condition.clone(), generate_condition.clone()); + instances_from_generate_block( + &generate.nodes.2, + then_condition, + syntax_tree, + const_env, + packed_dimensions, + instances, + )?; + if let Some((_, block)) = &generate.nodes.3 { + let else_condition = combine_conditions( + condition, + ConstExpr::Unary { + op: UnaryOp::LogicNot, + expr: Box::new(generate_condition), + }, + ); + instances_from_generate_block( + block, + else_condition, syntax_tree, - &type_aliases, const_env, - &mut signals, + packed_dimensions, + instances, )?; } - signals.sort_by(|a, b| a.name.cmp(&b.name)); - if let Some(name) = signals - .windows(2) - .find(|pair| pair[0].name == pair[1].name) - .map(|pair| pair[0].name.clone()) - { - return Err(AnalyzerError::Unsupported(format!( - "duplicate internal signal `{name}`" - ))); - } - Ok(signals) + Ok(()) } -fn signals_from_non_port_module_item( - item: &sv_parser::NonPortModuleItem, +fn instances_from_generate_block( + block: &sv_parser::GenerateBlock, + condition: Option, syntax_tree: &SyntaxTree, - type_aliases: &HashMap, const_env: &HashMap, - signals: &mut Vec, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, ) -> Result<(), AnalyzerError> { - match item { - sv_parser::NonPortModuleItem::GenerateRegion(region) => { - for item in ®ion.nodes.1 { - signals_from_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; - } - } - sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { - signals_from_module_or_generate_item( + match block { + sv_parser::GenerateBlock::GenerateItem(item) => { + instances_from_generate_item( item, + condition, syntax_tree, - type_aliases, const_env, - signals, + packed_dimensions, + instances, )?; } - _ => {} + sv_parser::GenerateBlock::Multiple(block) => { + let mut block_env = const_env.clone(); + for item in &block.nodes.3 { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + &packed_dimensions.type_aliases, + ) { + continue; + } + instances_from_generate_item( + item, + condition.clone(), + syntax_tree, + &block_env, + packed_dimensions, + instances, + )?; + } + } + } + Ok(()) +} + +fn combine_conditions(parent: Option, child: ConstExpr) -> Option { + Some(match parent { + Some(parent) => ConstExpr::Binary { + left: Box::new(parent), + op: BinaryOp::LogicAnd, + right: Box::new(child), + }, + None => child, + }) +} + +fn instances_from_module_instantiation( + instantiation: &sv_parser::ModuleInstantiation, + condition: Option, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + instances: &mut Vec, +) -> Result<(), AnalyzerError> { + let module_name = identifier_text( + RefNode::ModuleIdentifier(&instantiation.nodes.0), + syntax_tree, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) + })?; + let mut parameter_overrides = + parameter_overrides_from_value_assignment(instantiation.nodes.1.as_ref(), syntax_tree)?; + for override_ in &mut parameter_overrides { + override_.value = override_ + .value + .take() + .map(|value| substitute_const_expr_constants_preserving_enum_types(value, const_env)); + } + let condition = + condition.map(|condition| substitute_const_expr_constants(condition, const_env)); + let parameter_names: Vec = parameter_overrides + .iter() + .map(|parameter| parameter.name().to_string()) + .collect(); + for instance in instantiation.nodes.2.contents() { + let name = identifier_text( + RefNode::InstanceIdentifier(&instance.nodes.0.nodes.0), + syntax_tree, + ) + .ok_or_else(|| AnalyzerError::Unsupported("unsupported instance identifier".to_string()))?; + let mut port_connections = + port_connections_from_hierarchical_instance(instance, syntax_tree, packed_dimensions)?; + for connection in &mut port_connections { + connection.actual_expr = connection.actual_expr.take().map(|expr| { + substitute_expr_constants_with_parameter_literals( + expr, + const_env, + &HashMap::default(), + ) + }); + } + let port_names = port_connections + .iter() + .map(|connection| connection.formal().to_string()) + .collect(); + instances.push(Instance::new( + module_name.clone(), + name, + parameter_names.clone(), + parameter_overrides.clone(), + condition.clone(), + port_names, + port_connections, + )); } Ok(()) } -fn signals_from_generate_item( - item: &sv_parser::GenerateItem, +fn parameter_overrides_from_value_assignment( + assignment: Option<&sv_parser::ParameterValueAssignment>, syntax_tree: &SyntaxTree, - type_aliases: &HashMap, - const_env: &HashMap, - signals: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { - signals_from_module_or_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; +) -> Result, AnalyzerError> { + let Some(assignment) = assignment else { + return Ok(Vec::new()); + }; + let Some(assignments) = assignment.nodes.1.nodes.1.as_ref() else { + return Ok(Vec::new()); + }; + let sv_parser::ListOfParameterAssignments::Named(assignments) = assignments else { + return Err(AnalyzerError::Unsupported( + "ordered parameter assignment".to_string(), + )); + }; + let mut overrides = Vec::new(); + let mut names = HashSet::default(); + for assignment in assignments.nodes.0.contents() { + let name = identifier_text( + RefNode::ParameterIdentifier(&assignment.nodes.1), + syntax_tree, + ) + .ok_or_else(|| AnalyzerError::Unsupported("parameter override identifier".to_string()))?; + if !names.insert(name.clone()) { + return Err(AnalyzerError::Unsupported(format!( + "duplicate parameter override `{name}`" + ))); + } + let value = match assignment.nodes.2.nodes.1.as_ref() { + Some(expr) => Some( + const_expr_from_param_expression(expr, syntax_tree).ok_or_else(|| { + AnalyzerError::Unsupported("parameter override expression".to_string()) + })?, + ), + None => { + return Err(AnalyzerError::Unsupported(format!( + "empty parameter override `{name}`" + ))); + } + }; + overrides.push(ParameterOverride::new(name, value)); } - Ok(()) + Ok(overrides) } -fn signals_from_module_or_generate_item( - item: &sv_parser::ModuleOrGenerateItem, +fn port_connections_from_hierarchical_instance( + instance: &sv_parser::HierarchicalInstance, syntax_tree: &SyntaxTree, - type_aliases: &HashMap, - const_env: &HashMap, - signals: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::ModuleOrGenerateItem::Module(module) => { - let mut alias_signals = - signals_from_type_alias_instantiation(&module.nodes.1, syntax_tree, type_aliases)?; - substitute_signal_local_constants(&mut alias_signals, const_env); - signals.extend(alias_signals); + packed_dimensions: &PackedDimensions, +) -> Result, AnalyzerError> { + let Some(connections) = instance.nodes.1.nodes.1.as_ref() else { + return Ok(Vec::new()); + }; + let sv_parser::ListOfPortConnections::Named(connections) = connections else { + if let sv_parser::ListOfPortConnections::Ordered(connections) = connections + && connections + .nodes + .0 + .contents() + .iter() + .all(|connection| connection.nodes.1.is_none()) + { + return Ok(Vec::new()); } - sv_parser::ModuleOrGenerateItem::ModuleItem(item) => { - signals_from_module_common_item( - &item.nodes.1, - syntax_tree, - type_aliases, - const_env, - signals, - )?; + return Err(AnalyzerError::Unsupported( + "ordered port connection".to_string(), + )); + }; + let mut lowered = Vec::new(); + for connection in connections.nodes.0.contents() { + match connection { + sv_parser::NamedPortConnection::Identifier(connection) => { + let formal = + identifier_text(RefNode::PortIdentifier(&connection.nodes.2), syntax_tree) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "named port connection identifier".to_string(), + ) + })?; + let actual_expr = match connection.nodes.3.as_ref() { + None => Some(Expr::Ident(formal.clone())), + Some(paren) => match paren.nodes.1.as_ref() { + None => None, + Some(expr) => Some( + expr_from_expression_with_types(expr, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "named port connection expression".to_string(), + ) + })?, + ), + }, + }; + let actual = actual_expr + .as_ref() + .and_then(expr_ident_name) + .unwrap_or_else(|| formal.clone()); + lowered.push(PortConnection::new(formal, actual, actual_expr)); + } + sv_parser::NamedPortConnection::Asterisk(_) => {} } - _ => {} } - Ok(()) + Ok(lowered) } -fn signals_from_module_common_item( - item: &sv_parser::ModuleCommonItem, - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, - const_env: &HashMap, - signals: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) => { - let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration( - declaration, - ) = &**declaration - else { - return Ok(()); - }; - let mut declared = match &**declaration { - sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) => { - signals_from_data_declaration(data, syntax_tree, type_aliases)? - } - sv_parser::PackageOrGenerateItemDeclaration::NetDeclaration(net) => { - signals_from_net_declaration(net, syntax_tree, type_aliases)? - } - _ => Vec::new(), - }; - substitute_signal_local_constants(&mut declared, const_env); - signals.extend(declared); - } - sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { - let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { - return Ok(()); - }; - let condition = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1), - syntax_tree, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("conditional-generate condition lowering".to_string()) - })?; - let condition = eval_ast_const_expr(&condition, const_env).ok_or_else(|| { - AnalyzerError::Unsupported("unknown conditional-generate condition".to_string()) - })?; - let block = if condition != 0 { - Some(&generate.nodes.2) - } else { - generate.nodes.3.as_ref().map(|(_, block)| block) - }; - if let Some(block) = block { - signals_from_generate_block(block, syntax_tree, type_aliases, const_env, signals)?; - } - } - _ => {} +fn expr_ident_name(expr: &Expr) -> Option { + match expr { + Expr::Ident(name) => Some(name.clone()), + _ => None, } - Ok(()) } -fn signals_from_generate_block( - block: &sv_parser::GenerateBlock, +fn identifier_text(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Option { + let locate = identifier_locate(node)?; + syntax_tree.get_str(&locate).map(str::to_string) +} + +fn functions_from_module_node( + node: RefNode<'_>, syntax_tree: &SyntaxTree, - type_aliases: &HashMap, const_env: &HashMap, - signals: &mut Vec, -) -> Result<(), AnalyzerError> { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => { - signals_from_generate_item(item, syntax_tree, type_aliases, const_env, signals)?; + packed_dimensions: &PackedDimensions, +) -> Result, AnalyzerError> { + let mut functions = HashMap::default(); + let type_aliases = + type_aliases_from_module_node_with_env(node.clone(), syntax_tree, const_env)?; + let inactive_nodes = + inactive_conditional_generate_nodes(node.clone(), syntax_tree, const_env, &type_aliases); + for child in node { + if inactive_nodes.iter().any(|inactive| inactive == &child) { + continue; } - sv_parser::GenerateBlock::Multiple(block) => { - let mut block_env = const_env.clone(); - for item in &block.nodes.3 { - if add_localparams_from_generate_item(item, syntax_tree, &mut block_env) { - continue; - } - signals_from_generate_item(item, syntax_tree, type_aliases, &block_env, signals)?; + let RefNode::FunctionDeclaration(declaration) = child else { + continue; + }; + validate_function_return_type(declaration, syntax_tree, const_env, &type_aliases)?; + validate_function_formal_types(declaration, syntax_tree, const_env, &type_aliases)?; + validate_function_local_names(declaration, syntax_tree, const_env, &type_aliases)?; + validate_function_declaration_statements( + declaration, + syntax_tree, + const_env, + &type_aliases, + packed_dimensions, + )?; + if let Some(function) = function_from_declaration( + declaration, + syntax_tree, + const_env, + &type_aliases, + packed_dimensions, + ) { + let name = function.name.clone(); + let mut parameter_names = HashSet::default(); + if let Some(parameter) = function + .params + .iter() + .find(|parameter| !parameter_names.insert(parameter.name.as_str())) + { + return Err(AnalyzerError::Unsupported(format!( + "duplicate function argument `{}`", + parameter.name + ))); + } + if functions.insert(name.clone(), function).is_some() { + return Err(AnalyzerError::Unsupported(format!( + "duplicate function declaration `{name}`" + ))); } } } - Ok(()) + Ok(functions) } -fn substitute_signal_local_constants(signals: &mut [Signal], const_env: &HashMap) { - for signal in signals { - for range in &mut signal.r#type.packed_ranges { - range.left = substitute_const_expr_constants(range.left.clone(), const_env); - range.right = substitute_const_expr_constants(range.right.clone(), const_env); +fn validate_function_local_names( + declaration: &sv_parser::FunctionDeclaration, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Result<(), AnalyzerError> { + let (params, local_types) = match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => { + let params = body + .nodes + .3 + .nodes + .1 + .as_ref() + .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) + .unwrap_or_default(); + let local_types = function_local_types_from_block_items( + &body.nodes.5, + syntax_tree, + const_env, + type_aliases, + ) + .unwrap_or_default(); + (params, local_types) } - for range in &mut signal.r#type.unpacked_ranges { - range.left = substitute_const_expr_constants(range.left.clone(), const_env); - range.right = substitute_const_expr_constants(range.right.clone(), const_env); - range.size = range - .size - .take() - .map(|size| substitute_const_expr_constants(size, const_env)); + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { + let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); + let block_items = body.nodes.4.iter().filter_map(|item| match item { + sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), + sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, + }); + let local_types = function_local_types_from_block_item_iter( + block_items, + syntax_tree, + const_env, + type_aliases, + ) + .unwrap_or_default(); + (params, local_types) } + }; + if let Some(param) = params + .iter() + .find(|param| local_types.contains_key(¶m.name)) + { + Err(AnalyzerError::Unsupported(format!( + "function local shadows formal `{}`", + param.name + ))) + } else { + Ok(()) } } -fn signals_from_net_declaration( - net: &sv_parser::NetDeclaration, +fn validate_function_return_type( + declaration: &sv_parser::FunctionDeclaration, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, -) -> Result, AnalyzerError> { - let (r#type, assignments, is_net) = match net { - sv_parser::NetDeclaration::NetType(net) => { - let r#type = type_from_ref_node(RefNode::DataTypeOrImplicit(&net.nodes.3), syntax_tree) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(&net.nodes.3), - syntax_tree, - type_aliases, - ) - }); - let r#type = match (&net.nodes.3, r#type) { - (_, Some(r#type)) => r#type, - (sv_parser::DataTypeOrImplicit::ImplicitDataType(_), None) => Type::implicit(), - (sv_parser::DataTypeOrImplicit::DataType(_), None) => { - return Err(AnalyzerError::Unsupported( - "unsupported net data type".to_string(), - )); - } - }; - let r#type = type_with_fallback_ranges( - r#type, - RefNode::DataTypeOrImplicit(&net.nodes.3), - syntax_tree, - type_aliases, - ); - (r#type, net.nodes.5.nodes.0.contents(), true) - } - sv_parser::NetDeclaration::NetTypeIdentifier(net) => { - let Some(name) = identifier_text(RefNode::NetTypeIdentifier(&net.nodes.0), syntax_tree) - else { - return Ok(Vec::new()); - }; - let Some(r#type) = type_aliases.get(&name).cloned() else { - return Ok(Vec::new()); +) -> Result<(), AnalyzerError> { + let node = match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => &body.nodes.0, + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => &body.nodes.0, + }; + let unsupported = match node { + sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => { + let sv_parser::DataTypeOrVoid::DataType(data_type) = &**data_type else { + return Ok(()); }; - (r#type, net.nodes.2.nodes.0.contents(), false) - } - sv_parser::NetDeclaration::Interconnect(_) => { - return Err(AnalyzerError::Unsupported( - "interconnect net declaration".to_string(), - )); + let r#type = type_from_ref_node(RefNode::DataType(data_type), syntax_tree) + .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)); + r#type + .as_ref() + .is_some_and(|r#type| !r#type.unpacked_ranges().is_empty()) + || value_type_from_data_type(data_type, syntax_tree, const_env, type_aliases) + .is_none() } + sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(_) => false, }; - let mut signals = Vec::new(); - for assignment in assignments { - let name = identifier_text(RefNode::NetIdentifier(&assignment.nodes.0), syntax_tree) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported signal identifier".to_string()) - })?; - let signal_type = type_with_unpacked_ranges( - r#type.clone(), - unpacked_ranges_from_dimensions(&assignment.nodes.1, syntax_tree)?, - ); - signals.push(if is_net { - Signal::new_net(name, signal_type) - } else { - Signal::new(name, signal_type) - }); + if unsupported { + Err(AnalyzerError::Unsupported( + "unsupported function return data type or unpacked dimension".to_string(), + )) + } else { + Ok(()) } - Ok(signals) } -fn signals_from_type_alias_instantiation( - instantiation: &sv_parser::ModuleInstantiation, +fn validate_function_formal_types( + declaration: &sv_parser::FunctionDeclaration, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, -) -> Result, AnalyzerError> { - let mut signals = Vec::new(); - let module_name = identifier_text( - RefNode::ModuleIdentifier(&instantiation.nodes.0), - syntax_tree, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) - })?; - let Some(r#type) = type_aliases.get(&module_name) else { - return Ok(signals); +) -> Result<(), AnalyzerError> { + let unsupported = |node: &sv_parser::DataTypeOrImplicit| { + matches!(node, sv_parser::DataTypeOrImplicit::DataType(_)) + && value_type_from_data_type_or_implicit(node, syntax_tree, const_env, type_aliases) + .is_none() }; - for instance in instantiation.nodes.2.contents() { - let name = identifier_text( - RefNode::InstanceIdentifier(&instance.nodes.0.nodes.0), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("unsupported signal identifier".to_string()))?; - let signal_type = type_with_unpacked_ranges( - r#type.clone(), - unpacked_ranges_from_dimensions(&instance.nodes.0.nodes.1, syntax_tree)?, - ); - signals.push(Signal::new(name, signal_type)); + let has_unpacked_dimensions = + |node: &sv_parser::DataTypeOrImplicit, dimensions: &[sv_parser::VariableDimension]| { + !dimensions.is_empty() + || type_from_ref_node(RefNode::DataTypeOrImplicit(node), syntax_tree) + .or_else(|| { + type_alias_from_ref_node( + RefNode::DataTypeOrImplicit(node), + syntax_tree, + type_aliases, + ) + }) + .is_some_and(|r#type| !r#type.unpacked_ranges().is_empty()) + }; + let invalid = match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => { + body.nodes.3.nodes.1.as_ref().is_some_and(|ports| { + ports + .nodes + .0 + .contents() + .iter() + // A type-only item can be the parser's representation of + // the shorthand `input logic a, b`; only validate entries + // that actually carry a formal identifier here. + .any(|port| { + port.nodes.4.as_ref().is_some_and(|(_, dimensions, _)| { + unsupported(&port.nodes.3) + || has_unpacked_dimensions(&port.nodes.3, dimensions) + }) + }) + }) + } + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => body.nodes.4.iter().any(|item| { + let sv_parser::TfItemDeclaration::TfPortDeclaration(port) = item else { + return false; + }; + unsupported(&port.nodes.3) + || port + .nodes + .4 + .nodes + .0 + .contents() + .iter() + .any(|(_, dimensions, _)| has_unpacked_dimensions(&port.nodes.3, dimensions)) + }), + }; + if invalid { + Err(AnalyzerError::Unsupported( + "unsupported function formal data type or unpacked dimension".to_string(), + )) + } else { + Ok(()) } - Ok(signals) } -fn type_aliases_from_module_node( - node: RefNode<'_>, +fn validate_function_declaration_statements( + declaration: &sv_parser::FunctionDeclaration, syntax_tree: &SyntaxTree, -) -> Result, AnalyzerError> { - let mut aliases = HashMap::default(); - if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { - if let RefNode::ParameterPortList(parameter_port_list) = parameter_port_list { - add_type_aliases_from_parameter_port_list( - parameter_port_list, + const_env: &HashMap, + type_aliases: &HashMap, + packed_dimensions: &PackedDimensions, +) -> Result<(), AnalyzerError> { + let (statements, params, local_types) = match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => { + let params = body + .nodes + .3 + .nodes + .1 + .as_ref() + .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) + .unwrap_or_default(); + let local_types = function_local_types_from_block_items( + &body.nodes.5, syntax_tree, - &mut aliases, - ); + const_env, + type_aliases, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported function local data type".to_string()) + })?; + (&body.nodes.6, params, local_types) } - for child in parameter_port_list { - match child { - RefNode::LocalParameterDeclaration(localparam) => { - add_type_aliases_from_localparam(localparam, syntax_tree, &mut aliases); - } - RefNode::ParameterDeclaration(parameter) => { - add_type_aliases_from_parameter(parameter, syntax_tree, &mut aliases); - } - _ => {} - } + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { + let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); + let block_items = body.nodes.4.iter().filter_map(|item| match item { + sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), + sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, + }); + let local_types = function_local_types_from_block_item_iter( + block_items, + syntax_tree, + const_env, + type_aliases, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported function local data type".to_string()) + })?; + (&body.nodes.5, params, local_types) } - } - for item in module_non_port_items(node.clone()) { - let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { + }; + let mut assignment_targets = local_types.into_keys().collect::>(); + assignment_targets.extend(params.into_iter().map(|param| param.name)); + for node in RefNode::FunctionDeclaration(declaration) { + let RefNode::BlockingAssignment(assignment) = node else { continue; }; - match declaration { - sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(declaration) => { - add_type_alias_from_data_declaration(declaration, syntax_tree, &mut aliases)?; - } - sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) => { - add_type_aliases_from_localparam(&localparam.0, syntax_tree, &mut aliases); - } - sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { - add_type_aliases_from_parameter(¶meter.0, syntax_tree, &mut aliases); - } - _ => {} + let lhs = match assignment { + sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.0, + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => &assignment.nodes.0, + _ => continue, + }; + let Some(LValue::Ident(name)) = + variable_lvalue_from_node(lhs, syntax_tree, packed_dimensions) + else { + continue; + }; + if !assignment_targets.contains(&name) { + return Err(AnalyzerError::Unsupported(format!( + "function assignment target outside local scope `{name}`" + ))); } } - for child in node { - let RefNode::TypeAssignment(assignment) = child else { + for statement in statements { + let sv_parser::FunctionStatementOrNull::Statement(statement) = statement else { continue; }; - add_type_alias_from_type_assignment(assignment, syntax_tree, &mut aliases); + validate_function_statement(&statement.nodes.0, syntax_tree, packed_dimensions)?; } - Ok(aliases) + Ok(()) } -fn add_type_alias_from_data_declaration( - declaration: &sv_parser::DataDeclaration, +fn validate_function_statement_or_null( + statement: &sv_parser::StatementOrNull, syntax_tree: &SyntaxTree, - aliases: &mut HashMap, + packed_dimensions: &PackedDimensions, ) -> Result<(), AnalyzerError> { - let sv_parser::DataDeclaration::TypeDeclaration(declaration) = declaration else { - return Ok(()); - }; - let sv_parser::TypeDeclaration::DataType(declaration) = &**declaration else { - return Ok(()); - }; - let Some(name) = identifier_text(RefNode::TypeIdentifier(&declaration.nodes.2), syntax_tree) - else { - return Ok(()); - }; - let Some(r#type) = type_from_ref_node(RefNode::DataType(&declaration.nodes.1), syntax_tree) - else { + let sv_parser::StatementOrNull::Statement(statement) = statement else { return Ok(()); }; - let r#type = type_with_unpacked_ranges( - r#type, - unpacked_ranges_from_variable_dimensions(&declaration.nodes.3, syntax_tree)?, - ); - aliases.insert(name, r#type); - Ok(()) + validate_function_statement(statement, syntax_tree, packed_dimensions) } -fn add_type_aliases_from_parameter_port_list( - list: &sv_parser::ParameterPortList, +fn validate_function_statement( + statement: &sv_parser::Statement, syntax_tree: &SyntaxTree, - aliases: &mut HashMap, -) { - match list { - sv_parser::ParameterPortList::Assignment(list) => { - for (_, declaration) in &list.nodes.1.nodes.1.1 { - add_type_aliases_from_parameter_port_declaration(declaration, syntax_tree, aliases); - } - } - sv_parser::ParameterPortList::Declaration(list) => { - for declaration in list.nodes.1.nodes.1.contents() { - add_type_aliases_from_parameter_port_declaration(declaration, syntax_tree, aliases); - } + packed_dimensions: &PackedDimensions, +) -> Result<(), AnalyzerError> { + match &statement.nodes.2 { + sv_parser::StatementItem::JumpStatement(statement) + if matches!(&**statement, sv_parser::JumpStatement::Return(_)) => + { + let sv_parser::JumpStatement::Return(statement) = &**statement else { + unreachable!(); + }; + let expr = statement.nodes.1.as_ref().ok_or_else(|| { + AnalyzerError::Unsupported("expressionless function return".to_string()) + })?; + expr_from_expression_with_types(expr, syntax_tree, packed_dimensions).ok_or_else( + || AnalyzerError::Unsupported("unsupported function return expression".to_string()), + )?; + Ok(()) } - sv_parser::ParameterPortList::Empty(_) => {} - } -} - -fn add_type_aliases_from_parameter_port_declaration( - declaration: &sv_parser::ParameterPortDeclaration, - syntax_tree: &SyntaxTree, - aliases: &mut HashMap, -) { - match declaration { - sv_parser::ParameterPortDeclaration::ParameterDeclaration(declaration) => { - add_type_aliases_from_parameter(declaration, syntax_tree, aliases); + sv_parser::StatementItem::BlockingAssignment(assignment) => { + let rhs = match &assignment.0 { + sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.3, + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + &assignment.nodes.2 + } + _ => { + return Err(AnalyzerError::Unsupported( + "unsupported function assignment".to_string(), + )); + } + }; + expr_from_expression_with_types(rhs, syntax_tree, packed_dimensions).ok_or_else( + || { + AnalyzerError::Unsupported( + "unsupported function assignment expression".to_string(), + ) + }, + )?; + Ok(()) } - sv_parser::ParameterPortDeclaration::LocalParameterDeclaration(declaration) => { - add_type_aliases_from_localparam(declaration, syntax_tree, aliases); + sv_parser::StatementItem::SeqBlock(block) => { + for statement in &block.nodes.3 { + validate_function_statement_or_null(statement, syntax_tree, packed_dimensions)?; + } + Ok(()) } - sv_parser::ParameterPortDeclaration::TypeList(list) => { - for assignment in list.nodes.1.nodes.0.contents() { - add_type_alias_from_type_assignment(assignment, syntax_tree, aliases); + sv_parser::StatementItem::ConditionalStatement(statement) => { + expr_from_cond_predicate(&statement.nodes.2.nodes.1, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "unsupported function conditional predicate".to_string(), + ) + })?; + validate_function_statement_or_null( + &statement.nodes.3, + syntax_tree, + packed_dimensions, + )?; + for (_, _, predicate, branch) in &statement.nodes.4 { + expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "unsupported function conditional predicate".to_string(), + ) + })?; + validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; + } + if let Some((_, branch)) = &statement.nodes.5 { + validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; } + Ok(()) } - sv_parser::ParameterPortDeclaration::ParamList(_) => {} - } -} - -fn add_type_aliases_from_localparam( - declaration: &sv_parser::LocalParameterDeclaration, - syntax_tree: &SyntaxTree, - aliases: &mut HashMap, -) { - let sv_parser::LocalParameterDeclaration::Type(declaration) = declaration else { - return; - }; - for assignment in declaration.nodes.2.nodes.0.contents() { - add_type_alias_from_type_assignment(assignment, syntax_tree, aliases); - } -} - -fn add_type_aliases_from_parameter( - declaration: &sv_parser::ParameterDeclaration, - syntax_tree: &SyntaxTree, - aliases: &mut HashMap, -) { - let sv_parser::ParameterDeclaration::Type(declaration) = declaration else { - return; - }; - for assignment in declaration.nodes.2.nodes.0.contents() { - add_type_alias_from_type_assignment(assignment, syntax_tree, aliases); - } -} - -fn add_type_alias_from_type_assignment( - assignment: &sv_parser::TypeAssignment, - syntax_tree: &SyntaxTree, - aliases: &mut HashMap, -) { - let Some((_, data_type)) = &assignment.nodes.1 else { - return; - }; - let Some(name) = identifier_text(RefNode::TypeIdentifier(&assignment.nodes.0), syntax_tree) - else { - return; - }; - let Some(r#type) = type_from_ref_node(RefNode::DataType(data_type), syntax_tree) else { - return; - }; - aliases.insert(name, r#type); -} - -fn signals_from_data_declaration( - data: &sv_parser::DataDeclaration, - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, -) -> Result, AnalyzerError> { - let sv_parser::DataDeclaration::Variable(variable) = data else { - return Ok(Vec::new()); - }; - let r#type = type_from_ref_node(RefNode::DataTypeOrImplicit(&variable.nodes.3), syntax_tree) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(&variable.nodes.3), + sv_parser::StatementItem::CaseStatement(statement) => { + let sv_parser::CaseStatement::Normal(statement) = &**statement else { + return Err(AnalyzerError::Unsupported( + "unsupported statement inside function".to_string(), + )); + }; + expr_from_expression_with_types( + &statement.nodes.2.nodes.1.nodes.0, syntax_tree, - type_aliases, + packed_dimensions, ) - }); - let r#type = match (&variable.nodes.3, r#type) { - (_, Some(r#type)) => r#type, - (sv_parser::DataTypeOrImplicit::ImplicitDataType(_), None) => Type::implicit(), - (sv_parser::DataTypeOrImplicit::DataType(_), None) => { - return Err(AnalyzerError::Unsupported( - "unsupported internal data type".to_string(), - )); + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported function case selector".to_string()) + })?; + for item in std::iter::once(&statement.nodes.3).chain(statement.nodes.4.iter()) { + let branch = match item { + sv_parser::CaseItem::NonDefault(item) => { + for expr in item.nodes.0.contents() { + expr_from_expression_with_types( + &expr.nodes.0, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "unsupported function case item expression".to_string(), + ) + })?; + } + &item.nodes.2 + } + sv_parser::CaseItem::Default(item) => &item.nodes.2, + }; + validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; + } + Ok(()) } - }; - let r#type = type_with_fallback_ranges( - r#type, - RefNode::DataTypeOrImplicit(&variable.nodes.3), - syntax_tree, - type_aliases, - ); - let mut signals = Vec::new(); - for assignment in variable.nodes.4.nodes.0.contents() { - let sv_parser::VariableDeclAssignment::Variable(assignment) = assignment else { - continue; - }; - let name = identifier_text( - RefNode::VariableIdentifier(&assignment.nodes.0), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("unsupported signal identifier".to_string()))?; - let signal_type = type_with_unpacked_ranges( - r#type.clone(), - unpacked_ranges_from_variable_dimensions(&assignment.nodes.1, syntax_tree)?, - ); - signals.push(Signal::new(name, signal_type)); + _ => Err(AnalyzerError::Unsupported( + "unsupported statement inside function".to_string(), + )), } - Ok(signals) } -fn type_alias_from_ref_node( - node: RefNode<'_>, +fn function_from_declaration( + declaration: &sv_parser::FunctionDeclaration, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, -) -> Option { - let name = - if let Some(RefNode::DataTypeType(data_type)) = unwrap_node!(node.clone(), DataTypeType) { - identifier_text(RefNode::TypeIdentifier(&data_type.nodes.1), syntax_tree)? - } else { - let RefNode::TypeIdentifier(identifier) = unwrap_node!(node, TypeIdentifier)? else { - return None; - }; - identifier_text(RefNode::TypeIdentifier(identifier), syntax_tree)? - }; - type_aliases.get(&name).cloned() -} - -fn parameters_from_ref_node( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, - parameters: &mut Vec, - is_local: bool, -) -> Result<(), AnalyzerError> { - if node.clone().into_iter().any(|child| { - matches!( - child, - RefNode::DataTypeOrImplicit(sv_parser::DataTypeOrImplicit::DataType(data_type)) - if !matches!( - &**data_type, - sv_parser::DataType::Vector(_) - | sv_parser::DataType::Atom(_) - | sv_parser::DataType::Type(_) - | sv_parser::DataType::ClassType(_) - ) - ) - }) { - return Err(AnalyzerError::Unsupported( - "unsupported parameter data type".to_string(), - )); - } - let parameter_width = parameter_declared_width(node.clone(), syntax_tree, parameters); - let has_declared_type = node.clone().into_iter().any(|child| { - matches!( - child, - RefNode::DataTypeOrImplicit(sv_parser::DataTypeOrImplicit::DataType(_)) - ) - }); - let parameter_signed = parameter_width.map(|_| { - integer_atom_expr_type(node.clone()) - .map(|r#type| r#type.signed) - .unwrap_or_else(|| is_signed_from_ref_node(node.clone()).unwrap_or(false)) - }); - let parameter_is_2state = type_from_ref_node(node.clone(), syntax_tree) - .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); - for child in node { - if let RefNode::ParamAssignment(param) = child { - let name = parameter_name(RefNode::ParameterIdentifier(¶m.nodes.0), syntax_tree)?; - let mut value = param + packed_dimensions: &PackedDimensions, +) -> Option { + match &declaration.nodes.2 { + sv_parser::FunctionBodyDeclaration::WithPort(body) => { + let name = identifier_text(RefNode::FunctionIdentifier(&body.nodes.2), syntax_tree)?; + let params = body .nodes - .2 + .3 + .nodes + .1 .as_ref() - .and_then(|(_, expr)| const_expr_from_constant_param(expr, syntax_tree)); - value = - normalize_unbased_unsized_parameter_value(value, parameter_width, parameter_signed); - parameters.push(Parameter::new( + .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) + .unwrap_or_default(); + let mut local_types = function_local_types_from_block_items( + &body.nodes.5, + syntax_tree, + const_env, + type_aliases, + )?; + let mut function_packed_dimensions = packed_dimensions.clone(); + function_packed_dimensions.extend(params.iter().map(|param| { + ( + param.name.clone(), + VariableDimensions { + packed: param.packed_dimensions.clone(), + unpacked: Vec::new(), + signed: param.signed, + is_2state: param.is_2state, + }, + ) + })); + function_packed_dimensions.extend(function_local_packed_dimensions_from_block_items( + &body.nodes.5, + syntax_tree, + const_env, + type_aliases, + )?); + let local_names = local_types.keys().cloned().collect::>(); + insert_function_param_types(¶ms, &mut local_types); + let expr = function_body_expr( + &body.nodes.6, + syntax_tree, + &function_packed_dimensions, + &local_types, + &local_names, + )?; + let return_type = + function_return_type(&body.nodes.0, syntax_tree, const_env, type_aliases); + let return_first_packed_dimension_width = function_return_first_packed_dimension_width( + &body.nodes.0, + syntax_tree, + const_env, + type_aliases, + return_type, + ); + let return_is_2state = + function_return_is_2state(&body.nodes.0, syntax_tree, type_aliases); + Some(Function { name, - value, - parameter_width, - parameter_signed, - parameter_is_2state, - has_declared_type, - is_local, - )); - } - } - Ok(()) -} - -fn parameter_declared_width( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, - parameters: &[Parameter], -) -> Option { - let ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); - if ranges.is_empty() { - if let Some(r#type) = integer_atom_expr_type(node.clone()) { - return Some(r#type.width); - } - return unwrap_node!(node, IntegerVectorType).is_some().then_some(1); - } - let env = const_env_from_parameters(parameters); - ranges.iter().try_fold(1usize, |acc, range| { - let left = eval_ast_const_expr(range.left(), &env)?; - let right = eval_ast_const_expr(range.right(), &env)?; - acc.checked_mul(left.abs_diff(right) as usize + 1) - }) -} - -fn normalize_unbased_unsized_parameter_value( - value: Option, - width: Option, - signed: Option, -) -> Option { - let signing = if signed.unwrap_or(false) { "s" } else { "" }; - match (value, width) { - (Some(ConstExpr::Literal(value)), Some(width)) if value == "'1" => { - let literal = if width <= 128 { - let bits = if width == 128 { - u128::MAX - } else { - (1u128 << width) - 1 - }; - format!("{width}'{signing}d{bits}") - } else { - format!("{width}'{signing}b{}", "1".repeat(width)) - }; - Some(ConstExpr::Literal(literal)) - } - (Some(ConstExpr::Literal(value)), Some(width)) if value == "'0" => { - Some(ConstExpr::Literal(format!("{width}'{signing}d0"))) + params, + body: expr, + return_width: return_type.map(|r#type| r#type.width), + return_first_packed_dimension_width, + return_signed: return_type.is_some_and(|r#type| r#type.signed), + return_is_2state, + }) } - (Some(ConstExpr::Literal(value)), Some(width)) - if matches!(value.as_str(), "'x" | "'X" | "'z" | "'Z" | "'?") => - { - let fill = value.chars().nth(1)?; - Some(ConstExpr::Literal(format!("{width}'{signing}b{fill}"))) + sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { + let name = identifier_text(RefNode::FunctionIdentifier(&body.nodes.2), syntax_tree)?; + let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); + let block_items = body + .nodes + .4 + .iter() + .filter_map(|item| match item { + sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), + sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, + }) + .collect::>(); + let mut local_types = function_local_types_from_block_item_iter( + block_items.iter().copied(), + syntax_tree, + const_env, + type_aliases, + )?; + let mut function_packed_dimensions = packed_dimensions.clone(); + function_packed_dimensions.extend(params.iter().map(|param| { + ( + param.name.clone(), + VariableDimensions { + packed: param.packed_dimensions.clone(), + unpacked: Vec::new(), + signed: param.signed, + is_2state: param.is_2state, + }, + ) + })); + function_packed_dimensions.extend( + function_local_packed_dimensions_from_block_item_iter( + block_items.iter().copied(), + syntax_tree, + const_env, + type_aliases, + )?, + ); + let local_names = local_types.keys().cloned().collect::>(); + insert_function_param_types(¶ms, &mut local_types); + let expr = function_body_expr( + &body.nodes.5, + syntax_tree, + &function_packed_dimensions, + &local_types, + &local_names, + )?; + let return_type = + function_return_type(&body.nodes.0, syntax_tree, const_env, type_aliases); + let return_first_packed_dimension_width = function_return_first_packed_dimension_width( + &body.nodes.0, + syntax_tree, + const_env, + type_aliases, + return_type, + ); + let return_is_2state = + function_return_is_2state(&body.nodes.0, syntax_tree, type_aliases); + Some(Function { + name, + params, + body: expr, + return_width: return_type.map(|r#type| r#type.width), + return_first_packed_dimension_width, + return_signed: return_type.is_some_and(|r#type| r#type.signed), + return_is_2state, + }) } - (value, _) => value, } } -fn const_env_from_parameters(parameters: &[Parameter]) -> HashMap { - let mut env = HashMap::default(); - let mut parameter_types = HashMap::default(); - for parameter in parameters { - let Some(value) = parameter.resolved_value(&env, ¶meter_types) else { - continue; - }; - if let Some(r#type) = parameter.resolved_type(¶meter_types) { - parameter_types.insert(parameter.name().to_string(), r#type); - insert_parameter_type_markers(&mut env, parameter.name(), r#type); +fn insert_function_param_types( + params: &[FunctionParam], + local_types: &mut HashMap, +) { + for param in params { + if let Some(width) = param.width { + local_types.insert( + param.name.clone(), + FunctionLocalType { + width, + signed: param.signed, + is_2state: param.is_2state, + }, + ); } - env.insert(parameter.name().to_string(), value); - env.insert(parameter_marker(parameter.name()), value); } - env } -fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 { - if width >= 128 { - return value; - } - if width == 0 { - return 0; - } - let mask = (1u128 << width) - 1; - let bits = (value as u128) & mask; - if signed && bits & (1u128 << (width - 1)) != 0 { - (bits | !mask) as i128 - } else { - bits as i128 - } +fn function_local_types_from_block_items( + items: &[sv_parser::BlockItemDeclaration], + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option> { + function_local_types_from_block_item_iter(items.iter(), syntax_tree, const_env, type_aliases) } -fn parameter_value_env( - parameters: &[Parameter], +fn function_local_packed_dimensions_from_block_items( + items: &[sv_parser::BlockItemDeclaration], + syntax_tree: &SyntaxTree, const_env: &HashMap, -) -> HashMap { - let mut values = HashMap::default(); - let mut parameter_types = HashMap::default(); - for parameter in parameters { - let inferred_type = parameter.value().and_then(|value| { - infer_parameter_value_type(value, parameter.has_declared_type, ¶meter_types) - }); - let width = parameter - .declared_width - .or(inferred_type.map(|r#type| r#type.width)); - let signed = parameter - .declared_signed - .or(inferred_type.map(|r#type| r#type.signed)) - .unwrap_or(false); - if let Some(width) = width { - parameter_types.insert(parameter.name().to_string(), ExprType { width, signed }); - } - - let mut value = if let Some(value) = const_env.get(parameter.name()).copied() { - if let Some(width) = width { - Expr::Literal(format_typed_parameter_literal(value, width, signed)) - } else if value.is_negative() { - let width = (128 - (!value as u128).leading_zeros() as usize + 1).max(32); - let mask = if width == 128 { - u128::MAX - } else { - (1u128 << width) - 1 - }; - Expr::Literal(format!("{width}'sd{}", (value as u128) & mask)) - } else if let Some(ConstExpr::Literal(literal)) = parameter.value() { - Expr::Literal(literal.clone()) - } else { - Expr::Literal(value.to_string()) - } - } else if let Some(value) = parameter.value().cloned() { - let value = substitute_typed_parameter_literals(value, const_env, ¶meter_types); - let value = replace_oob_const_selects_with_unknown(value, const_env); - let value = substitute_expr_constants_with_parameter_literals( - const_expr_to_expr(value), - const_env, - &values, - ); - if let Some(width) = width { - Expr::Resize { - expr: Box::new(value), - width, - signed, - } - } else { - value - } - } else { - continue; - }; - if parameter.declared_is_2state { - value = Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(value), - }; - } - values.insert(parameter.name().to_string(), value); - } - values + type_aliases: &HashMap, +) -> Option { + function_local_packed_dimensions_from_block_item_iter( + items.iter(), + syntax_tree, + const_env, + type_aliases, + ) } -fn replace_oob_const_selects_with_unknown( - expr: ConstExpr, +fn function_local_packed_dimensions_from_block_item_iter<'a>( + items: impl IntoIterator, + syntax_tree: &SyntaxTree, const_env: &HashMap, -) -> ConstExpr { - match expr { - ConstExpr::Select { expr, bit } => { - let expr = replace_oob_const_selects_with_unknown(*expr, const_env); - let bit = replace_oob_const_selects_with_unknown(*bit, const_env); - if let ConstExpr::Literal(literal) = &expr - && let Some(width) = - typecheck::parse_integral_literal(literal).map(|literal| literal.width) - && match typecheck::eval_const_expr(&bit.clone().into(), const_env) { - Some(bit_index) => { - usize::try_from(bit_index).map_or(true, |bit_index| bit_index >= width) - } - None => const_expr_contains_unknown_literal(&bit), - } - { - ConstExpr::Literal("1'bx".to_string()) - } else { - ConstExpr::Select { - expr: Box::new(expr), - bit: Box::new(bit), - } - } - } - ConstExpr::Function { name, args } => ConstExpr::Function { - name, - args: args - .into_iter() - .map(|arg| replace_oob_const_selects_with_unknown(arg, const_env)) - .collect(), - }, - ConstExpr::Unary { op, expr } => ConstExpr::Unary { - op, - expr: Box::new(replace_oob_const_selects_with_unknown(*expr, const_env)), - }, - ConstExpr::Binary { left, op, right } => ConstExpr::Binary { - left: Box::new(replace_oob_const_selects_with_unknown(*left, const_env)), - op, - right: Box::new(replace_oob_const_selects_with_unknown(*right, const_env)), - }, - ConstExpr::Mux { - condition, - then_expr, - else_expr, - } => ConstExpr::Mux { - condition: Box::new(replace_oob_const_selects_with_unknown( - *condition, const_env, - )), - then_expr: Box::new(replace_oob_const_selects_with_unknown( - *then_expr, const_env, - )), - else_expr: Box::new(replace_oob_const_selects_with_unknown( - *else_expr, const_env, - )), - }, - ConstExpr::Ident(name) => ConstExpr::Ident(name), - ConstExpr::Literal(value) => ConstExpr::Literal(value), + type_aliases: &HashMap, +) -> Option { + let mut dimensions = HashMap::default(); + for item in items { + let sv_parser::BlockItemDeclaration::Data(item) = item else { + continue; + }; + let signals = + signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases, const_env) + .ok()?; + dimensions.extend(signals.into_iter().map(|signal| { + ( + signal.name().to_string(), + VariableDimensions { + packed: function_packed_dimension_widths(signal.r#type().packed_ranges()), + unpacked: unpacked_dimension_widths(signal.r#type().unpacked_ranges()), + signed: signal.r#type().is_signed(), + is_2state: signal.r#type().kind() == TypeKind::Bit, + }, + ) + })); } -} - -fn const_expr_contains_unknown_literal(expr: &ConstExpr) -> bool { - match expr { - ConstExpr::Literal(literal) => typecheck::parse_integral_literal(literal) - .is_some_and(|literal| literal.mask != Default::default()), - ConstExpr::Ident(_) => false, - ConstExpr::Select { expr, bit } => { - const_expr_contains_unknown_literal(expr) || const_expr_contains_unknown_literal(bit) - } - ConstExpr::Function { args, .. } => args.iter().any(const_expr_contains_unknown_literal), - ConstExpr::Unary { expr, .. } => const_expr_contains_unknown_literal(expr), - ConstExpr::Binary { left, right, .. } => { - const_expr_contains_unknown_literal(left) || const_expr_contains_unknown_literal(right) - } - ConstExpr::Mux { - condition, - then_expr, - else_expr, - } => { - const_expr_contains_unknown_literal(condition) - || const_expr_contains_unknown_literal(then_expr) - || const_expr_contains_unknown_literal(else_expr) + Some(dimensions) +} + +fn function_local_types_from_block_item_iter<'a>( + items: impl IntoIterator, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option> { + let mut local_types = HashMap::default(); + for item in items { + let sv_parser::BlockItemDeclaration::Data(item) = item else { + continue; + }; + let signals = + signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases, const_env) + .ok()?; + for signal in signals { + let r#type = signal.r#type(); + if !r#type.unpacked_ranges().is_empty() { + return None; + } + let width = if r#type.packed_ranges().is_empty() { + 1 + } else { + r#type + .packed_ranges() + .iter() + .try_fold(1usize, |acc, range| { + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + acc.checked_mul(left.abs_diff(right) as usize + 1) + })? + }; + local_types.insert( + signal.name().to_string(), + FunctionLocalType { + width, + signed: r#type.is_signed(), + is_2state: r#type.kind() == TypeKind::Bit, + }, + ); } } + Some(local_types) } -fn const_expr_to_expr(expr: ConstExpr) -> Expr { - match expr { - ConstExpr::Literal(value) => Expr::Literal(value), - ConstExpr::Ident(name) => Expr::Ident(name), - ConstExpr::Select { expr, bit } => Expr::Select { - expr: Box::new(const_expr_to_expr(*expr)), - msb: (*bit).clone(), - lsb: *bit, - signed: false, - }, - ConstExpr::Function { name, args } => Expr::Call { - name, - args: args.into_iter().map(const_expr_to_expr).collect(), - }, - ConstExpr::Unary { op, expr } => Expr::Unary { - op, - expr: Box::new(const_expr_to_expr(*expr)), - }, - ConstExpr::Binary { left, op, right } => Expr::Binary { - left: Box::new(const_expr_to_expr(*left)), - op, - right: Box::new(const_expr_to_expr(*right)), - }, - ConstExpr::Mux { - condition, - then_expr, - else_expr, - } => Expr::Mux { - condition: Box::new(const_expr_to_expr(*condition)), - then_expr: Box::new(const_expr_to_expr(*then_expr)), - else_expr: Box::new(const_expr_to_expr(*else_expr)), +fn function_return_is_2state( + node: &sv_parser::FunctionDataTypeOrImplicit, + syntax_tree: &SyntaxTree, + type_aliases: &HashMap, +) -> bool { + let r#type = match node { + sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { + sv_parser::DataTypeOrVoid::DataType(data_type) => { + type_from_ref_node(RefNode::DataType(data_type), syntax_tree) + .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)) + } + sv_parser::DataTypeOrVoid::Void(_) => None, }, - } + sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { + type_from_ref_node(RefNode::ImplicitDataType(data_type), syntax_tree).or_else(|| { + type_alias_from_ref_node( + RefNode::ImplicitDataType(data_type), + syntax_tree, + type_aliases, + ) + }) + } + }; + r#type.is_some_and(|r#type| r#type.kind() == TypeKind::Bit) } -fn format_typed_parameter_literal(value: i128, width: usize, signed: bool) -> String { - let signing = if signed { "s" } else { "" }; - if width <= 128 { - let mask = if width == 128 { - u128::MAX - } else { - (1u128 << width) - 1 - }; - let bits = (value as u128) & mask; - format!("{width}'{signing}d{bits}") - } else { - let extension = if value.is_negative() { '1' } else { '0' }; - let high_bits = extension.to_string().repeat(width - 128); - let low_bits = value as u128; - format!("{width}'{signing}b{high_bits}{low_bits:0128b}") +fn function_return_type( + node: &sv_parser::FunctionDataTypeOrImplicit, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + match node { + sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { + sv_parser::DataTypeOrVoid::DataType(data_type) => { + value_type_from_data_type(data_type, syntax_tree, const_env, type_aliases) + } + sv_parser::DataTypeOrVoid::Void(_) => None, + }, + sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { + value_type_from_ref_node( + RefNode::ImplicitDataType(data_type), + syntax_tree, + const_env, + type_aliases, + ) + } } } -#[derive(Clone, Copy)] -pub(crate) struct ExprType { - pub(crate) width: usize, - pub(crate) signed: bool, +fn function_return_first_packed_dimension_width( + node: &sv_parser::FunctionDataTypeOrImplicit, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + return_type: Option, +) -> Option { + let r#type = function_type_from_ref_node( + RefNode::FunctionDataTypeOrImplicit(node), + syntax_tree, + const_env, + type_aliases, + ); + let Some(first) = r#type + .as_ref() + .and_then(|r#type| r#type.packed_ranges().first()) + else { + return return_type.map(|r#type| r#type.width); + }; + let left = eval_ast_const_expr(first.left(), const_env)?; + let right = eval_ast_const_expr(first.right(), const_env)?; + usize::try_from(left.abs_diff(right)) + .ok() + .and_then(|width| width.checked_add(1)) } -fn substitute_typed_parameter_literals( - expr: ConstExpr, - constants: &HashMap, - parameter_types: &HashMap, -) -> ConstExpr { - match expr { - ConstExpr::Ident(name) => match (constants.get(&name), parameter_types.get(&name)) { - (Some(value), Some(r#type)) => ConstExpr::Literal(format_typed_parameter_literal( - *value, - r#type.width, - r#type.signed, - )), - _ => ConstExpr::Ident(name), - }, - ConstExpr::Literal(value) => ConstExpr::Literal(value), - ConstExpr::Select { expr, bit } => ConstExpr::Select { - expr: Box::new(substitute_typed_parameter_literals( - *expr, - constants, - parameter_types, - )), - bit: Box::new(substitute_typed_parameter_literals( - *bit, - constants, - parameter_types, - )), - }, - ConstExpr::Function { name, args } => ConstExpr::Function { +fn tf_params( + list: &sv_parser::TfPortList, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Vec { + let mut params = Vec::new(); + let mut previous_type = None; + let mut previous_is_2state = false; + let mut previous_packed_dimensions = Vec::new(); + for port in list.nodes.0.contents() { + let type_node = RefNode::DataTypeOrImplicit(&port.nodes.3); + let inferred_type = value_type_from_data_type_or_implicit( + &port.nodes.3, + syntax_tree, + const_env, + type_aliases, + ); + let inferred_is_2state = type_from_ref_node(type_node.clone(), syntax_tree) + .or_else(|| type_alias_from_ref_node(type_node.clone(), syntax_tree, type_aliases)) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); + let inferred_packed_dimensions = + function_param_packed_dimensions(&port.nodes.3, syntax_tree, const_env, type_aliases); + let omitted_type = matches!( + port.nodes.3, + sv_parser::DataTypeOrImplicit::ImplicitDataType(_) + ) && is_signed_from_ref_node(type_node.clone()).is_none() + && inferred_packed_dimensions.is_empty(); + let (name, r#type, is_2state, packed_dimensions) = + if let Some((identifier, _, _)) = port.nodes.4.as_ref() { + let Some(name) = identifier_text(RefNode::PortIdentifier(identifier), syntax_tree) + else { + continue; + }; + let r#type = if port.nodes.1.is_none() && omitted_type { + previous_type.or(inferred_type) + } else { + inferred_type + }; + let is_2state = if port.nodes.1.is_none() && omitted_type { + previous_is_2state + } else { + inferred_is_2state + }; + let packed_dimensions = if port.nodes.1.is_none() && omitted_type { + previous_packed_dimensions.clone() + } else { + inferred_packed_dimensions + }; + (name, r#type, is_2state, packed_dimensions) + } else { + // An identifier following a comma is syntactically ambiguous with a + // user-defined type. sv-parser represents the shorthand `a, b` as a + // type-only item, so reinterpret an unknown type name as the next + // parameter and inherit the preceding item's type. + if type_alias_from_data_type_or_implicit(&port.nodes.3, syntax_tree, type_aliases) + .is_some() + { + continue; + } + let Some(name) = identifier_text(type_node, syntax_tree) else { + continue; + }; + let r#type = if port.nodes.1.is_none() { + previous_type + } else { + Some(ExprType { + width: 1, + signed: false, + }) + }; + let is_2state = port.nodes.1.is_none() && previous_is_2state; + let packed_dimensions = if port.nodes.1.is_none() { + previous_packed_dimensions.clone() + } else { + inferred_packed_dimensions + }; + (name, r#type, is_2state, packed_dimensions) + }; + previous_type = r#type; + previous_is_2state = is_2state; + previous_packed_dimensions = packed_dimensions.clone(); + params.push(FunctionParam { name, - args: args - .into_iter() - .map(|arg| substitute_typed_parameter_literals(arg, constants, parameter_types)) - .collect(), - }, - ConstExpr::Unary { op, expr } => ConstExpr::Unary { - op, - expr: Box::new(substitute_typed_parameter_literals( - *expr, - constants, - parameter_types, - )), - }, - ConstExpr::Binary { left, op, right } => ConstExpr::Binary { - left: Box::new(substitute_typed_parameter_literals( - *left, - constants, - parameter_types, - )), - op, - right: Box::new(substitute_typed_parameter_literals( - *right, - constants, - parameter_types, - )), - }, - ConstExpr::Mux { - condition, - then_expr, - else_expr, - } => ConstExpr::Mux { - condition: Box::new(substitute_typed_parameter_literals( - *condition, - constants, - parameter_types, - )), - then_expr: Box::new(substitute_typed_parameter_literals( - *then_expr, - constants, - parameter_types, - )), - else_expr: Box::new(substitute_typed_parameter_literals( - *else_expr, - constants, - parameter_types, - )), - }, + width: r#type.map(|r#type| r#type.width), + signed: r#type.is_some_and(|r#type| r#type.signed), + is_2state, + packed_dimensions, + }); } + params } -fn infer_const_expr_type( - expr: &ConstExpr, - parameter_types: &HashMap, -) -> Option { - match expr { - ConstExpr::Literal(literal) => { - let literal = typecheck::parse_integral_literal(literal)?; - Some(ExprType { - width: literal.width, - signed: literal.signed, - }) - } - ConstExpr::Ident(name) => parameter_types.get(name).copied(), - ConstExpr::Select { .. } => Some(ExprType { - width: 1, - signed: false, - }), - ConstExpr::Function { name, .. } => match name.as_str() { - "$clog2" => Some(ExprType { - width: 32, - signed: true, - }), - "$onehot" | "$onehot0" => Some(ExprType { - width: 1, - signed: false, - }), - _ => None, - }, - ConstExpr::Unary { op, expr } => { - let operand = infer_const_expr_type(expr, parameter_types)?; - if matches!( - op, - UnaryOp::LogicNot | UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor - ) { - Some(ExprType { - width: 1, - signed: false, - }) - } else { - Some(operand) - } - } - ConstExpr::Binary { left, op, right } => { - let left = infer_const_expr_type(left, parameter_types)?; - let right = infer_const_expr_type(right, parameter_types)?; - match op { - BinaryOp::LogicAnd - | BinaryOp::LogicOr - | BinaryOp::Eq - | BinaryOp::Ne - | BinaryOp::EqCase - | BinaryOp::NeCase - | BinaryOp::EqWildcard - | BinaryOp::NeWildcard - | BinaryOp::Lt - | BinaryOp::Le - | BinaryOp::Gt - | BinaryOp::Ge => Some(ExprType { - width: 1, - signed: false, - }), - BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar => Some(left), - _ => Some(ExprType { - width: left.width.max(right.width), - signed: left.signed && right.signed, - }), - } - } - ConstExpr::Mux { - then_expr, - else_expr, - .. - } => { - let then_type = infer_const_expr_type(then_expr, parameter_types)?; - let else_type = infer_const_expr_type(else_expr, parameter_types)?; - Some(ExprType { - width: then_type.width.max(else_type.width), - signed: then_type.signed && else_type.signed, - }) +fn tf_item_params( + items: &[sv_parser::TfItemDeclaration], + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Vec { + let mut params = Vec::new(); + for item in items { + let sv_parser::TfItemDeclaration::TfPortDeclaration(declaration) = item else { + continue; + }; + let r#type = value_type_from_data_type_or_implicit( + &declaration.nodes.3, + syntax_tree, + const_env, + type_aliases, + ); + let is_2state = type_from_ref_node( + RefNode::DataTypeOrImplicit(&declaration.nodes.3), + syntax_tree, + ) + .or_else(|| { + type_alias_from_ref_node( + RefNode::DataTypeOrImplicit(&declaration.nodes.3), + syntax_tree, + type_aliases, + ) + }) + .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); + let packed_dimensions = function_param_packed_dimensions( + &declaration.nodes.3, + syntax_tree, + const_env, + type_aliases, + ); + for (identifier, _, _) in declaration.nodes.4.nodes.0.contents() { + let Some(name) = identifier_text(RefNode::PortIdentifier(identifier), syntax_tree) + else { + continue; + }; + params.push(FunctionParam { + name, + width: r#type.map(|r#type| r#type.width), + signed: r#type.is_some_and(|r#type| r#type.signed), + is_2state, + packed_dimensions: packed_dimensions.clone(), + }); } } + params } -fn infer_parameter_value_type( - value: &ConstExpr, - has_declared_type: bool, - parameter_types: &HashMap, +fn value_type_from_data_type_or_implicit( + node: &sv_parser::DataTypeOrImplicit, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { - if !has_declared_type - && let ConstExpr::Literal(literal) = value - && matches!( - literal.trim(), - "'0" | "'1" | "'x" | "'X" | "'z" | "'Z" | "'?" - ) - { - Some(ExprType { - width: 1, - signed: false, - }) - } else { - infer_const_expr_type(value, parameter_types) - } + value_type_from_ref_node( + RefNode::DataTypeOrImplicit(node), + syntax_tree, + const_env, + type_aliases, + ) } -#[derive(Debug, Clone, PartialEq, Eq)] -struct PackedDimension { - left: ConstExpr, - right: ConstExpr, - width: ConstExpr, - normalize_single: bool, +fn value_type_from_data_type( + node: &sv_parser::DataType, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + value_type_from_ref_node( + RefNode::DataType(node), + syntax_tree, + const_env, + type_aliases, + ) } -#[derive(Debug, Clone, PartialEq, Eq)] -struct UnpackedDimension { - left: ConstExpr, - right: ConstExpr, - width: ConstExpr, +fn value_type_from_ref_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + let r#type = function_type_from_ref_node(node, syntax_tree, const_env, type_aliases)?; + expr_type_from_type(&r#type, const_env) } -#[derive(Debug, Clone, PartialEq, Eq)] -struct VariableDimensions { - packed: Vec, - unpacked: Vec, - signed: bool, +fn function_type_from_ref_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { + let node = match node { + RefNode::DataTypeOrImplicit(node) => match node { + sv_parser::DataTypeOrImplicit::DataType(node) => RefNode::DataType(node), + sv_parser::DataTypeOrImplicit::ImplicitDataType(node) => { + RefNode::ImplicitDataType(node) + } + }, + RefNode::FunctionDataTypeOrImplicit(node) => match node { + sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(node) => match &**node { + sv_parser::DataTypeOrVoid::DataType(node) => RefNode::DataType(node), + sv_parser::DataTypeOrVoid::Void(_) => return None, + }, + sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(node) => { + RefNode::ImplicitDataType(node) + } + }, + node => node, + }; + // Only the declared type can supply an alias. A typedef used in a range + // bound's cast must not cause the built-in type's dimensions to be added twice. + let r#type = match &node { + RefNode::DataType(data_type) => { + let Some(alias) = type_alias_from_data_type(data_type, syntax_tree, type_aliases) + else { + return type_from_ref_node_with_env(node, syntax_tree, const_env, type_aliases); + }; + alias + } + RefNode::ImplicitDataType(_) => Type::implicit(), + _ => return None, + }; + Some(type_with_fallback_ranges_with_env( + r#type, + node, + syntax_tree, + const_env, + type_aliases, + )) } -type VariablePackedDimensions = HashMap; - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -struct PackedDimensions { - variables: VariablePackedDimensions, - const_env: HashMap, - type_aliases: HashMap, +fn integer_atom_expr_type(node: RefNode<'_>) -> Option { + let atom = unwrap_node!(node.clone(), IntegerAtomType)?; + let RefNode::IntegerAtomType(atom) = atom else { + return None; + }; + let (width, default_signed) = match atom { + sv_parser::IntegerAtomType::Byte(_) => (8, true), + sv_parser::IntegerAtomType::Shortint(_) => (16, true), + sv_parser::IntegerAtomType::Int(_) | sv_parser::IntegerAtomType::Integer(_) => (32, true), + sv_parser::IntegerAtomType::Longint(_) => (64, true), + sv_parser::IntegerAtomType::Time(_) => (64, false), + }; + Some(ExprType { + width, + signed: is_signed_from_ref_node(node).unwrap_or(default_signed), + }) } -impl PackedDimensions { - fn new( - variables: VariablePackedDimensions, - const_env: &HashMap, - type_aliases: &HashMap, - ) -> Self { - Self { - variables, - const_env: const_env.clone(), - type_aliases: type_aliases.clone(), +fn function_body_expr( + statements: &[sv_parser::FunctionStatementOrNull], + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, + local_names: &HashSet, +) -> Option { + let mut locals = local_types + .iter() + .map(|(name, r#type)| { + let initial = if local_names.contains(name) { + coerce_function_local_assignment(Expr::Literal("'x".to_string()), *r#type) + } else { + Expr::Ident(name.clone()) + }; + (name.clone(), initial) + }) + .collect::>(); + for statement in statements { + if let Some(expr) = function_expr_from_statement_or_null( + statement, + &mut locals, + syntax_tree, + packed_dimensions, + local_types, + ) { + return Some(expr); } } + None } -impl Deref for PackedDimensions { - type Target = VariablePackedDimensions; +fn function_expr_from_statement_or_null( + statement: &sv_parser::FunctionStatementOrNull, + locals: &mut HashMap, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, +) -> Option { + let sv_parser::FunctionStatementOrNull::Statement(statement) = statement else { + return None; + }; + function_expr_from_statement( + &statement.nodes.0, + locals, + syntax_tree, + packed_dimensions, + local_types, + ) +} - fn deref(&self) -> &Self::Target { - &self.variables - } +fn function_expr_from_statement_or_null_stmt( + statement: &sv_parser::StatementOrNull, + locals: &mut HashMap, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, +) -> Option { + let sv_parser::StatementOrNull::Statement(statement) = statement else { + return None; + }; + function_expr_from_statement( + statement, + locals, + syntax_tree, + packed_dimensions, + local_types, + ) } -impl DerefMut for PackedDimensions { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.variables +fn function_expr_from_statement( + statement: &sv_parser::Statement, + locals: &mut HashMap, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, +) -> Option { + match &statement.nodes.2 { + sv_parser::StatementItem::JumpStatement(statement) => { + let sv_parser::JumpStatement::Return(statement) = &**statement else { + return None; + }; + let expr = statement.nodes.1.as_ref()?; + expr_from_expression_with_types(expr, syntax_tree, packed_dimensions) + .map(|expr| substitute_expr_idents(expr, locals)) + } + sv_parser::StatementItem::BlockingAssignment(assignment) => { + let (lhs, rhs) = match &assignment.0 { + sv_parser::BlockingAssignment::Variable(assignment) => ( + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions), + expr_from_expression_with_types( + &assignment.nodes.3, + syntax_tree, + packed_dimensions, + ), + ), + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + let lhs = variable_lvalue_from_node( + &assignment.nodes.0, + syntax_tree, + packed_dimensions, + ); + let rhs = expr_from_expression_with_types( + &assignment.nodes.2, + syntax_tree, + packed_dimensions, + ); + let op = syntax_tree.get_str(&assignment.nodes.1.nodes.0.nodes.0); + let rhs = match (&lhs, rhs, op) { + (_, Some(rhs), Some("=")) => Some(rhs), + (Some(lhs), Some(rhs), Some(op)) => { + assignment_op_expr(lhs, op, rhs, packed_dimensions) + } + _ => None, + }; + (lhs, rhs) + } + _ => (None, None), + }; + let Some(LValue::Ident(name)) = lhs else { + return None; + }; + if let Some(rhs) = rhs { + let rhs = substitute_expr_idents(rhs, locals); + let rhs = local_types + .get(&name) + .copied() + .map(|r#type| coerce_function_local_assignment(rhs.clone(), r#type)) + .unwrap_or(rhs); + locals.insert(name, rhs); + } + None + } + sv_parser::StatementItem::SeqBlock(block) => { + let mut block_locals = locals.clone(); + for statement in &block.nodes.3 { + if let Some(expr) = function_expr_from_statement_or_null_stmt( + statement, + &mut block_locals, + syntax_tree, + packed_dimensions, + local_types, + ) { + return Some(expr); + } + } + *locals = block_locals; + None + } + sv_parser::StatementItem::ConditionalStatement(statement) => { + function_expr_from_conditional_statement( + statement, + locals, + syntax_tree, + packed_dimensions, + local_types, + ) + } + sv_parser::StatementItem::CaseStatement(statement) => function_expr_from_case_statement( + statement, + locals, + syntax_tree, + packed_dimensions, + local_types, + ), + _ => None, } } -fn packed_dimensions_from_ports_and_signals( - ports: &[Port], - signals: &[Signal], - const_env: &HashMap, - type_aliases: &HashMap, -) -> PackedDimensions { - let mut dimensions = HashMap::default(); - for port in ports { - dimensions.insert( - port.name().to_string(), - VariableDimensions { - packed: packed_dimension_widths(port.r#type().packed_ranges()), - unpacked: unpacked_dimension_widths(port.r#type().unpacked_ranges()), - signed: port.r#type().is_signed(), - }, - ); - } - for signal in signals { - dimensions.insert( - signal.name().to_string(), - VariableDimensions { - packed: packed_dimension_widths(signal.r#type().packed_ranges()), - unpacked: unpacked_dimension_widths(signal.r#type().unpacked_ranges()), - signed: signal.r#type().is_signed(), - }, +fn function_expr_from_conditional_statement( + statement: &sv_parser::ConditionalStatement, + locals: &mut HashMap, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, +) -> Option { + let mut branches = Vec::new(); + let if_condition = + expr_from_cond_predicate(&statement.nodes.2.nodes.1, syntax_tree, packed_dimensions)?; + let mut then_locals = locals.clone(); + let then_expr = function_expr_from_statement_or_null_stmt( + &statement.nodes.3, + &mut then_locals, + syntax_tree, + packed_dimensions, + local_types, + ); + branches.push(( + procedural_truth_condition(substitute_expr_idents(if_condition, locals)), + then_expr, + then_locals, + )); + + for (_, _, predicate, branch) in &statement.nodes.4 { + let condition = + expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions)?; + let mut branch_locals = locals.clone(); + let branch_expr = function_expr_from_statement_or_null_stmt( + branch, + &mut branch_locals, + syntax_tree, + packed_dimensions, + local_types, ); + branches.push(( + procedural_truth_condition(substitute_expr_idents(condition, locals)), + branch_expr, + branch_locals, + )); } - PackedDimensions::new(dimensions, const_env, type_aliases) -} -fn packed_dimension_widths(ranges: &[PackedRange]) -> Vec { - ranges - .iter() - .map(|range| { - let left = range.left().clone(); - let right = range.right().clone(); - let width = |high: ConstExpr, low: ConstExpr| ConstExpr::Binary { - left: Box::new(ConstExpr::Binary { - left: Box::new(high), - op: BinaryOp::Sub, - right: Box::new(low), - }), - op: BinaryOp::Add, - right: Box::new(ConstExpr::Literal("1".to_string())), - }; - let width = ConstExpr::Mux { - condition: Box::new(ConstExpr::Binary { - left: Box::new(left.clone()), - op: BinaryOp::Ge, - right: Box::new(right.clone()), - }), - then_expr: Box::new(width(left.clone(), right.clone())), - else_expr: Box::new(width(right.clone(), left.clone())), - }; - PackedDimension { - left, - right, - width, - normalize_single: false, - } - }) - .collect() -} + let (else_expr, else_locals) = if let Some((_, branch)) = &statement.nodes.5 { + let mut branch_locals = locals.clone(); + ( + function_expr_from_statement_or_null_stmt( + branch, + &mut branch_locals, + syntax_tree, + packed_dimensions, + local_types, + ), + branch_locals, + ) + } else { + (None, locals.clone()) + }; -fn unpacked_dimension_widths(ranges: &[UnpackedRange]) -> Vec { - ranges - .iter() - .map(|range| { - let left = range.left().clone(); - let right = range.right().clone(); - let width = ConstExpr::Mux { - condition: Box::new(ConstExpr::Binary { - left: Box::new(left.clone()), - op: BinaryOp::Ge, - right: Box::new(right.clone()), - }), - then_expr: Box::new(ConstExpr::Binary { - left: Box::new(left.clone()), - op: BinaryOp::Sub, - right: Box::new(right.clone()), - }), - else_expr: Box::new(ConstExpr::Binary { - left: Box::new(right.clone()), - op: BinaryOp::Sub, - right: Box::new(left.clone()), - }), + if branches.iter().all(|(_, expr, _)| expr.is_some()) && else_expr.is_some() { + let mut result = else_expr?; + for (condition, branch_expr, _) in branches.into_iter().rev() { + result = Expr::Mux { + condition: Box::new(condition), + then_expr: Box::new(branch_expr?), + else_expr: Box::new(result), }; - UnpackedDimension { - left, - right, - width: add_expr(width, ConstExpr::Literal("1".to_string())), + } + return Some(result); + } + + if branches.iter().all(|(_, expr, _)| expr.is_none()) && else_expr.is_none() { + let mut merged = locals.clone(); + let mut names = locals.keys().cloned().collect::>(); + names.extend(else_locals.keys().cloned()); + names.extend( + branches + .iter() + .flat_map(|(_, _, branch_locals)| branch_locals.keys().cloned()), + ); + for name in names { + let mut value = else_locals + .get(&name) + .or_else(|| locals.get(&name)) + .cloned() + .unwrap_or_else(|| Expr::Ident(name.clone())); + for (condition, _, branch_locals) in branches.iter().rev() { + let branch_value = branch_locals + .get(&name) + .cloned() + .unwrap_or_else(|| value.clone()); + if branch_value != value { + value = Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(branch_value), + else_expr: Box::new(value), + }; + } } - }) - .collect() -} + merged.insert(name, value); + } + *locals = merged; + return None; + } -fn function_packed_dimension_widths(ranges: &[PackedRange]) -> Vec { - packed_dimension_widths(ranges) - .into_iter() - .map(|mut dimension| { - dimension.normalize_single = true; - dimension - }) - .collect() + Some(Expr::Call { + name: "$unsupported_mixed_function_conditional".to_string(), + args: Vec::new(), + }) } -fn packed_dimensions_from_ref_node( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, -) -> Vec { - if let Some(alias) = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases) { - function_packed_dimension_widths(alias.packed_ranges()) +fn coerce_function_local_assignment(expr: Expr, r#type: FunctionLocalType) -> Expr { + let expr = Expr::Resize { + expr: Box::new(expr), + width: r#type.width, + signed: r#type.signed, + }; + if r#type.is_2state { + Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(expr), + } } else { - function_packed_dimension_widths(&packed_ranges_from_ref_node(node, syntax_tree)) + expr } } -fn parameter_marker(name: &str) -> String { - format!("__parameter::{name}") -} - -fn parameter_width_marker(name: &str) -> String { - format!("__parameter::width::{name}") -} - -fn parameter_signed_marker(name: &str) -> String { - format!("__parameter::signed::{name}") -} - -fn insert_parameter_type_markers( - const_env: &mut HashMap, - name: &str, - r#type: ExprType, -) { - if let Ok(width) = i128::try_from(r#type.width) { - const_env.insert(parameter_width_marker(name), width); - const_env.insert(parameter_signed_marker(name), r#type.signed as i128); +fn procedural_truth_condition(condition: Expr) -> Expr { + Expr::Unary { + op: UnaryOp::RedOr, + expr: Box::new(Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(condition), + }), } } -fn parameter_types_from_const_env(const_env: &HashMap) -> HashMap { - const PREFIX: &str = "__parameter::width::"; - const_env - .iter() - .filter_map(|(marker, width)| { - let name = marker.strip_prefix(PREFIX)?; - let width = usize::try_from(*width).ok()?; - let signed = const_env - .get(¶meter_signed_marker(name)) - .is_some_and(|signed| *signed != 0); - Some((name.to_string(), ExprType { width, signed })) - }) - .collect() -} - -fn module_parameter_port_list(node: RefNode<'_>) -> Option> { - match node { - RefNode::ModuleDeclarationAnsi(module) => module - .nodes - .0 - .nodes - .5 - .as_ref() - .map(RefNode::ParameterPortList), - RefNode::ModuleDeclarationNonansi(module) => module - .nodes - .0 - .nodes - .5 - .as_ref() - .map(RefNode::ParameterPortList), - _ => None, +fn function_expr_from_case_statement( + statement: &sv_parser::CaseStatement, + locals: &mut HashMap, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, + local_types: &HashMap, +) -> Option { + let sv_parser::CaseStatement::Normal(statement) = statement else { + return None; + }; + let case_expr = expr_from_expression_with_types( + &statement.nodes.2.nodes.1.nodes.0, + syntax_tree, + packed_dimensions, + )?; + let case_expr = substitute_expr_idents(case_expr, locals); + let mut default_branch = (None, locals.clone()); + let mut branches = Vec::new(); + for item in std::iter::once(&statement.nodes.3).chain(statement.nodes.4.iter()) { + match item { + sv_parser::CaseItem::NonDefault(item) => { + let mut branch_locals = locals.clone(); + let branch_expr = function_expr_from_statement_or_null_stmt( + &item.nodes.2, + &mut branch_locals, + syntax_tree, + packed_dimensions, + local_types, + ); + let conditions = item + .nodes + .0 + .contents() + .into_iter() + .filter_map(|expr| { + expr_from_expression_with_types( + &expr.nodes.0, + syntax_tree, + packed_dimensions, + ) + }) + .map(|expr| substitute_expr_idents(expr, locals)) + .map(|expr| case_item_condition(case_expr.clone(), expr)) + .collect::>(); + let condition = conditions.into_iter().reduce(|left, right| Expr::Binary { + left: Box::new(left), + op: BinaryOp::LogicOr, + right: Box::new(right), + })?; + branches.push((condition, branch_expr, branch_locals)); + } + sv_parser::CaseItem::Default(item) => { + let mut branch_locals = locals.clone(); + default_branch = ( + function_expr_from_statement_or_null_stmt( + &item.nodes.2, + &mut branch_locals, + syntax_tree, + packed_dimensions, + local_types, + ), + branch_locals, + ); + } + } } -} -fn module_non_port_items(node: RefNode<'_>) -> Vec<&sv_parser::NonPortModuleItem> { - match node { - RefNode::ModuleDeclarationAnsi(module) => module.nodes.2.iter().collect(), - RefNode::ModuleDeclarationNonansi(_) => Vec::new(), - _ => Vec::new(), + if branches.iter().all(|(_, expr, _)| expr.is_some()) && default_branch.0.is_some() { + let mut result = default_branch.0?; + for (condition, branch_expr, _) in branches.into_iter().rev() { + result = Expr::Mux { + condition: Box::new(condition), + then_expr: Box::new(branch_expr?), + else_expr: Box::new(result), + }; + } + return Some(result); } -} -fn package_or_generate_declaration_from_non_port_item( - item: &sv_parser::NonPortModuleItem, -) -> Option<&sv_parser::PackageOrGenerateItemDeclaration> { - let sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) = item else { - return None; - }; - let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { - return None; - }; - let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 - else { - return None; - }; - let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = - &**declaration - else { + if branches.iter().all(|(_, expr, _)| expr.is_none()) && default_branch.0.is_none() { + let mut merged = locals.clone(); + let mut names = locals.keys().cloned().collect::>(); + names.extend(default_branch.1.keys().cloned()); + names.extend( + branches + .iter() + .flat_map(|(_, _, branch_locals)| branch_locals.keys().cloned()), + ); + for name in names { + let mut value = default_branch + .1 + .get(&name) + .or_else(|| locals.get(&name)) + .cloned() + .unwrap_or_else(|| Expr::Ident(name.clone())); + for (condition, _, branch_locals) in branches.iter().rev() { + let branch_value = branch_locals + .get(&name) + .cloned() + .unwrap_or_else(|| value.clone()); + if branch_value != value { + value = Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(branch_value), + else_expr: Box::new(value), + }; + } + } + merged.insert(name, value); + } + *locals = merged; return None; - }; - Some(declaration) -} + } -fn parameter_name(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Result { - let locate = identifier_locate(node).ok_or_else(|| { - AnalyzerError::Unsupported("unsupported parameter identifier".to_string()) - })?; - syntax_tree - .get_str(&locate) - .map(str::to_string) - .ok_or_else(|| AnalyzerError::Unsupported("invalid parameter identifier span".to_string())) + Some(Expr::Call { + name: "$unsupported_mixed_function_case".to_string(), + args: Vec::new(), + }) } -fn port_name(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Result { - let locate = identifier_locate(node) - .ok_or_else(|| AnalyzerError::Unsupported("unsupported port identifier".to_string()))?; - syntax_tree - .get_str(&locate) - .map(str::to_string) - .ok_or_else(|| AnalyzerError::Unsupported("invalid port identifier span".to_string())) +fn case_item_condition(case_expr: Expr, item_expr: Expr) -> Expr { + Expr::Binary { + left: Box::new(case_expr), + op: BinaryOp::EqCase, + right: Box::new(item_expr), + } } -fn instances_from_module_node( +fn comb_processes_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - for child in node.clone() { - let RefNode::ModuleInstantiation(instantiation) = child else { - continue; - }; - let module_name = identifier_text( - RefNode::ModuleIdentifier(&instantiation.nodes.0), - syntax_tree, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) - })?; - if !type_aliases.contains_key(&module_name) - && instantiation - .nodes - .2 - .contents() - .iter() - .any(|instance| !instance.nodes.0.nodes.1.is_empty()) - { - return Err(AnalyzerError::Unsupported( - "module instance array".to_string(), - )); - } - } - let mut instances = Vec::new(); + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, +) -> Result, AnalyzerError> { + let mut processes = Vec::new(); for item in module_non_port_items(node) { - instances_from_non_port_module_item( + comb_processes_from_non_port_module_item( item, None, syntax_tree, const_env, packed_dimensions, - &mut instances, + functions, + expression_signedness, + parameter_literals, + &mut processes, )?; } - instances.retain(|instance| !type_aliases.contains_key(instance.module_name())); - Ok(instances) + Ok(processes) } -fn instances_from_non_port_module_item( +fn comb_processes_from_non_port_module_item( item: &sv_parser::NonPortModuleItem, condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, - instances: &mut Vec, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { sv_parser::NonPortModuleItem::GenerateRegion(region) => { for item in ®ion.nodes.1 { - instances_from_generate_item( + comb_processes_from_generate_item( item, condition.clone(), syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } } sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { - instances_from_module_or_generate_item( + comb_processes_from_module_or_generate_item( item, condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } _ => {} @@ -3823,114 +7259,203 @@ fn instances_from_non_port_module_item( Ok(()) } -fn instances_from_generate_item( +fn comb_processes_from_generate_item( item: &sv_parser::GenerateItem, condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, - instances: &mut Vec, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { - instances_from_module_or_generate_item( + comb_processes_from_module_or_generate_item( item, condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } Ok(()) } -fn instances_from_module_or_generate_item( +fn comb_processes_from_module_or_generate_item( item: &sv_parser::ModuleOrGenerateItem, condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, - instances: &mut Vec, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, +) -> Result<(), AnalyzerError> { + if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { + comb_processes_from_module_common_item( + &item.nodes.1, + condition, + syntax_tree, + const_env, + packed_dimensions, + functions, + expression_signedness, + parameter_literals, + processes, + )?; + } + Ok(()) +} + +fn comb_processes_from_module_common_item( + item: &sv_parser::ModuleCommonItem, + condition: Option, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { - sv_parser::ModuleOrGenerateItem::Module(module) => { - instances_from_module_instantiation( - &module.nodes.1, + sv_parser::ModuleCommonItem::ContinuousAssign(assign) => { + processes.extend( + assignments_from_continuous_assign(assign, syntax_tree, packed_dimensions)? + .into_iter() + .map(|assignment| substitute_assignment_constants(assignment, const_env)) + .map(|assignment| { + CombProcess::new( + CombProcessKind::ContinuousAssign, + condition.clone().map(|condition| { + substitute_const_expr_constants(condition, const_env) + }), + vec![assignment], + ) + }), + ); + } + sv_parser::ModuleCommonItem::AlwaysConstruct(always) => { + let mut local_packed_dimensions = packed_dimensions.clone(); + local_packed_dimensions.const_env = const_env.clone(); + if let Some(process) = comb_process_from_always_construct( + always, condition, syntax_tree, - const_env, - packed_dimensions, - instances, - )?; + &local_packed_dimensions, + functions, + expression_signedness, + parameter_literals, + )? { + processes.push(substitute_process_constants(process, const_env)); + } } - sv_parser::ModuleOrGenerateItem::ModuleItem(item) => { - instances_from_module_common_item( - &item.nodes.1, + sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { + comb_processes_from_conditional_generate( + generate, condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } - _ => {} - } - Ok(()) -} - -fn instances_from_module_common_item( - item: &sv_parser::ModuleCommonItem, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - instances: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { - instances_from_conditional_generate( + sv_parser::ModuleCommonItem::LoopGenerateConstruct(generate) => { + comb_processes_from_loop_generate( generate, condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } - sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(_) => {} + sv_parser::ModuleCommonItem::NetAlias(_) => { + return Err(AnalyzerError::Unsupported( + "module-level net alias".to_string(), + )); + } _ => {} } Ok(()) } -fn instances_from_conditional_generate( +fn comb_processes_from_conditional_generate( generate: &sv_parser::ConditionalGenerateConstruct, condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, - instances: &mut Vec, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { return Ok(()); }; - let Some(generate_condition) = const_expr_from_ref_node( + let Some(generate_condition) = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&generate.nodes.1.nodes.1), syntax_tree, + const_env, + &packed_dimensions.type_aliases, ) else { return Err(AnalyzerError::Unsupported( "conditional-generate condition lowering".to_string(), )); }; + if let Some(value) = eval_ast_const_expr(&generate_condition, const_env) { + let block = if value != 0 { + Some(&generate.nodes.2) + } else { + generate.nodes.3.as_ref().map(|(_, block)| block) + }; + if let Some(block) = block { + comb_processes_from_generate_block( + block, + condition, + syntax_tree, + const_env, + packed_dimensions, + functions, + expression_signedness, + parameter_literals, + processes, + )?; + } + return Ok(()); + } + if has_local_constants(const_env) + && eval_ast_const_expr(&generate_condition, const_env).is_none() + { + return Err(AnalyzerError::Unsupported( + "unknown conditional-generate condition".to_string(), + )); + } let generate_condition = substitute_const_expr_constants(generate_condition, const_env); let then_condition = combine_conditions(condition.clone(), generate_condition.clone()); - instances_from_generate_block( + comb_processes_from_generate_block( &generate.nodes.2, then_condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; if let Some((_, block)) = &generate.nodes.3 { let else_condition = combine_conditions( @@ -3940,3658 +7465,5470 @@ fn instances_from_conditional_generate( expr: Box::new(generate_condition), }, ); - instances_from_generate_block( + comb_processes_from_generate_block( block, else_condition, syntax_tree, const_env, packed_dimensions, - instances, + functions, + expression_signedness, + parameter_literals, + processes, )?; } Ok(()) } -fn instances_from_generate_block( - block: &sv_parser::GenerateBlock, +fn comb_processes_from_loop_generate( + generate: &sv_parser::LoopGenerateConstruct, condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, - instances: &mut Vec, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => { - instances_from_generate_item( - item, - condition, - syntax_tree, - const_env, - packed_dimensions, - instances, - )?; - } - sv_parser::GenerateBlock::Multiple(block) => { - let mut block_env = const_env.clone(); - for item in &block.nodes.3 { - if add_localparams_from_generate_item(item, syntax_tree, &mut block_env) { - continue; - } - instances_from_generate_item( - item, - condition.clone(), - syntax_tree, - &block_env, - packed_dimensions, - instances, - )?; - } - } + if generate_block_has_data_declaration(&generate.nodes.2) { + return Ok(()); } - Ok(()) -} - -fn combine_conditions(parent: Option, child: ConstExpr) -> Option { - Some(match parent { - Some(parent) => ConstExpr::Binary { - left: Box::new(parent), - op: BinaryOp::LogicAnd, - right: Box::new(child), - }, - None => child, - }) -} - -fn instances_from_module_instantiation( - instantiation: &sv_parser::ModuleInstantiation, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - instances: &mut Vec, -) -> Result<(), AnalyzerError> { - let module_name = identifier_text( - RefNode::ModuleIdentifier(&instantiation.nodes.0), + let name = identifier_text( + RefNode::GenvarIdentifier(&generate.nodes.1.nodes.1.0.nodes.1), syntax_tree, ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported module instantiation identifier".to_string()) - })?; - let mut parameter_overrides = - parameter_overrides_from_value_assignment(instantiation.nodes.1.as_ref(), syntax_tree)?; - for override_ in &mut parameter_overrides { - override_.value = override_ - .value - .take() - .map(|value| substitute_const_expr_constants(value, const_env)); - } - let condition = - condition.map(|condition| substitute_const_expr_constants(condition, const_env)); - let parameter_names: Vec = parameter_overrides - .iter() - .map(|parameter| parameter.name().to_string()) - .collect(); - for instance in instantiation.nodes.2.contents() { - let name = identifier_text( - RefNode::InstanceIdentifier(&instance.nodes.0.nodes.0), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("unsupported instance identifier".to_string()))?; - let mut port_connections = - port_connections_from_hierarchical_instance(instance, syntax_tree, packed_dimensions)?; - for connection in &mut port_connections { - connection.actual_expr = connection.actual_expr.take().map(|expr| { - substitute_expr_constants_with_parameter_literals( - expr, - const_env, - &HashMap::default(), - ) - }); + .ok_or_else(|| AnalyzerError::Unsupported("loop-generate variable".to_string()))?; + let init_expr = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1.0.nodes.3), + syntax_tree, + const_env, + &packed_dimensions.type_aliases, + ) + .ok_or_else(|| AnalyzerError::Unsupported("loop-generate initializer".to_string()))?; + let init = eval_ast_const_expr(&init_expr, const_env) + .ok_or_else(|| AnalyzerError::Unsupported("loop-generate initializer".to_string()))?; + let condition_expr = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1.2.nodes.0), + syntax_tree, + const_env, + &packed_dimensions.type_aliases, + ) + .ok_or_else(|| AnalyzerError::Unsupported("loop-generate condition".to_string()))?; + + let mut value = init; + let mut iterations = 0; + for _ in 0..10_000 { + let mut loop_env = const_env.clone(); + loop_env.insert(name.clone(), value); + let condition_value = eval_ast_const_expr(&condition_expr, &loop_env) + .ok_or_else(|| AnalyzerError::Unsupported("loop-generate condition".to_string()))?; + if condition_value == 0 { + break; } - let port_names = port_connections - .iter() - .map(|connection| connection.formal().to_string()) - .collect(); - instances.push(Instance::new( - module_name.clone(), - name, - parameter_names.clone(), - parameter_overrides.clone(), + comb_processes_from_generate_block( + &generate.nodes.2, condition.clone(), - port_names, - port_connections, - )); - } - Ok(()) -} - -fn parameter_overrides_from_value_assignment( - assignment: Option<&sv_parser::ParameterValueAssignment>, - syntax_tree: &SyntaxTree, -) -> Result, AnalyzerError> { - let Some(assignment) = assignment else { - return Ok(Vec::new()); - }; - let Some(assignments) = assignment.nodes.1.nodes.1.as_ref() else { - return Ok(Vec::new()); - }; - let sv_parser::ListOfParameterAssignments::Named(assignments) = assignments else { - return Err(AnalyzerError::Unsupported( - "ordered parameter assignment".to_string(), - )); - }; - let mut overrides = Vec::new(); - let mut names = HashSet::default(); - for assignment in assignments.nodes.0.contents() { - let name = identifier_text( - RefNode::ParameterIdentifier(&assignment.nodes.1), syntax_tree, + &loop_env, + packed_dimensions, + functions, + expression_signedness, + parameter_literals, + processes, + )?; + iterations += 1; + let next = next_genvar_value( + value, + &generate.nodes.1.nodes.1.4, + syntax_tree, + &loop_env, + &packed_dimensions.type_aliases, ) - .ok_or_else(|| AnalyzerError::Unsupported("parameter override identifier".to_string()))?; - if !names.insert(name.clone()) { - return Err(AnalyzerError::Unsupported(format!( - "duplicate parameter override `{name}`" - ))); - } - let value = match assignment.nodes.2.nodes.1.as_ref() { - Some(expr) => Some( - const_expr_from_param_expression(expr, syntax_tree).ok_or_else(|| { - AnalyzerError::Unsupported("parameter override expression".to_string()) - })?, - ), - None => { - return Err(AnalyzerError::Unsupported(format!( - "empty parameter override `{name}`" - ))); - } - }; - overrides.push(ParameterOverride::new(name, value)); + .ok_or_else(|| AnalyzerError::Unsupported("genvar update operator".to_string()))?; + value = next; } - Ok(overrides) -} - -fn port_connections_from_hierarchical_instance( - instance: &sv_parser::HierarchicalInstance, - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - let Some(connections) = instance.nodes.1.nodes.1.as_ref() else { - return Ok(Vec::new()); - }; - let sv_parser::ListOfPortConnections::Named(connections) = connections else { - if let sv_parser::ListOfPortConnections::Ordered(connections) = connections - && connections - .nodes - .0 - .contents() - .iter() - .all(|connection| connection.nodes.1.is_none()) - { - return Ok(Vec::new()); - } + let mut loop_env = const_env.clone(); + loop_env.insert(name, value); + if iterations == 10_000 + && eval_ast_const_expr(&condition_expr, &loop_env).is_some_and(|value| value != 0) + { return Err(AnalyzerError::Unsupported( - "ordered port connection".to_string(), + "loop-generate unroll limit exceeded".to_string(), )); - }; - let mut lowered = Vec::new(); - for connection in connections.nodes.0.contents() { - match connection { - sv_parser::NamedPortConnection::Identifier(connection) => { - let formal = - identifier_text(RefNode::PortIdentifier(&connection.nodes.2), syntax_tree) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "named port connection identifier".to_string(), - ) - })?; - let actual_expr = match connection.nodes.3.as_ref() { - None => Some(Expr::Ident(formal.clone())), - Some(paren) => match paren.nodes.1.as_ref() { - None => None, - Some(expr) => Some( - expr_from_expression_with_types(expr, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "named port connection expression".to_string(), - ) - })?, - ), - }, - }; - let actual = actual_expr - .as_ref() - .and_then(expr_ident_name) - .unwrap_or_else(|| formal.clone()); - lowered.push(PortConnection::new(formal, actual, actual_expr)); - } - sv_parser::NamedPortConnection::Asterisk(_) => {} - } } - Ok(lowered) + Ok(()) } -fn expr_ident_name(expr: &Expr) -> Option { - match expr { - Expr::Ident(name) => Some(name.clone()), - _ => None, +fn generate_block_has_data_declaration(block: &sv_parser::GenerateBlock) -> bool { + match block { + sv_parser::GenerateBlock::GenerateItem(item) => generate_item_has_data_declaration(item), + sv_parser::GenerateBlock::Multiple(block) => { + block.nodes.3.iter().any(generate_item_has_data_declaration) + } } } -fn identifier_text(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Option { - let locate = identifier_locate(node)?; - syntax_tree.get_str(&locate).map(str::to_string) +fn generate_block_has_type_declaration(block: &sv_parser::GenerateBlock) -> bool { + RefNode::GenerateBlock(block).into_iter().any(|node| { + matches!( + node, + RefNode::DataDeclaration(sv_parser::DataDeclaration::TypeDeclaration(_)) + | RefNode::TypeAssignment(_) + ) + }) } -fn functions_from_module_node( +fn has_local_constants(const_env: &HashMap) -> bool { + const_env.keys().any(|name| { + !name.starts_with("__parameter::") && !const_env.contains_key(¶meter_marker(name)) + }) +} + +fn reject_duplicate_conditional_generate_locals( node: RefNode<'_>, syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - let mut functions = HashMap::default(); - let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - let inactive_nodes = inactive_conditional_generate_nodes(node.clone(), syntax_tree, const_env); +) -> Result<(), AnalyzerError> { + let mut signal_names = HashSet::default(); + let mut parameter_names = HashSet::default(); for child in node { - if inactive_nodes.iter().any(|inactive| inactive == &child) { + let RefNode::ConditionalGenerateConstruct(sv_parser::ConditionalGenerateConstruct::If( + generate, + )) = child + else { continue; + }; + let blocks = std::iter::once(&generate.nodes.2).chain( + generate + .nodes + .3 + .as_ref() + .into_iter() + .map(|(_, block)| block), + ); + for block in blocks { + for name in generate_block_direct_data_declaration_names(block, syntax_tree) { + if signal_names.insert(name) { + continue; + } + return Err(AnalyzerError::Unsupported( + "local data declaration inside conditional-generate".to_string(), + )); + } + for name in generate_block_direct_local_parameter_names(block, syntax_tree) { + if parameter_names.insert(name) { + continue; + } + return Err(AnalyzerError::Unsupported( + "local data declaration inside conditional-generate".to_string(), + )); + } } - let RefNode::FunctionDeclaration(declaration) = child else { - continue; + } + Ok(()) +} + +fn conditional_generate_has_leaking_local(module: RefNode<'_>, syntax_tree: &SyntaxTree) -> bool { + let module_identifiers = module + .clone() + .into_iter() + .filter_map(identifier_locate) + .collect::>(); + module.into_iter().any(|node| { + let RefNode::ConditionalGenerateConstruct(sv_parser::ConditionalGenerateConstruct::If( + generate, + )) = node + else { + return false; }; - validate_function_return_type(declaration, syntax_tree, const_env, &type_aliases)?; - validate_function_formal_types(declaration, syntax_tree, const_env, &type_aliases)?; - validate_function_local_names(declaration, syntax_tree, const_env, &type_aliases)?; - validate_function_declaration_statements( - declaration, - syntax_tree, - const_env, - &type_aliases, - packed_dimensions, - )?; - if let Some(function) = function_from_declaration( - declaration, + std::iter::once(&generate.nodes.2) + .chain( + generate + .nodes + .3 + .as_ref() + .into_iter() + .map(|(_, block)| block), + ) + .any(|block| { + let block_identifiers = RefNode::GenerateBlock(block) + .into_iter() + .filter_map(identifier_locate) + .collect::>(); + let Some(block_start) = block_identifiers.iter().map(|locate| locate.offset).min() + else { + return false; + }; + let block_end = block_identifiers + .iter() + .map(|locate| locate.offset + locate.len) + .max() + .unwrap_or(block_start); + generate_block_direct_data_declaration_names(block, syntax_tree) + .into_iter() + .any(|name| { + module_identifiers.iter().any(|locate| { + (locate.offset < block_start || locate.offset >= block_end) + && syntax_tree.get_str(locate) == Some(name.as_str()) + }) + }) + }) + }) +} + +fn generate_block_direct_data_declaration_names( + block: &sv_parser::GenerateBlock, + syntax_tree: &SyntaxTree, +) -> Vec { + let aliases = HashMap::default(); + let mut names = Vec::new(); + visit_direct_generate_items(block, |item| { + let Some(sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data)) = + package_declaration_from_generate_item(item) + else { + return; + }; + names.extend( + signals_from_data_declaration(data, syntax_tree, &aliases, &HashMap::default()) + .unwrap_or_default() + .into_iter() + .map(|signal| signal.name), + ); + }); + names +} + +fn generate_block_direct_local_parameter_names( + block: &sv_parser::GenerateBlock, + syntax_tree: &SyntaxTree, +) -> Vec { + let mut names = Vec::new(); + visit_direct_generate_items(block, |item| { + let Some(sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration( + localparam, + )) = package_declaration_from_generate_item(item) + else { + return; + }; + let mut parameters = Vec::new(); + if parameters_from_ref_node( + RefNode::LocalParameterDeclaration(&localparam.0), syntax_tree, - const_env, - &type_aliases, - packed_dimensions, - ) { - let name = function.name.clone(); - let mut parameter_names = HashSet::default(); - if let Some(parameter) = function - .params - .iter() - .find(|parameter| !parameter_names.insert(parameter.name.as_str())) - { - return Err(AnalyzerError::Unsupported(format!( - "duplicate function argument `{}`", - parameter.name - ))); - } - if functions.insert(name.clone(), function).is_some() { - return Err(AnalyzerError::Unsupported(format!( - "duplicate function declaration `{name}`" - ))); + &mut parameters, + true, + &HashMap::default(), + &HashMap::default(), + &HashMap::default(), + ) + .is_ok() + { + names.extend(parameters.into_iter().map(|parameter| parameter.name)); + } + }); + names +} + +fn visit_direct_generate_items( + block: &sv_parser::GenerateBlock, + mut visit: impl FnMut(&sv_parser::GenerateItem), +) { + match block { + sv_parser::GenerateBlock::GenerateItem(item) => visit(item), + sv_parser::GenerateBlock::Multiple(block) => { + for item in &block.nodes.3 { + visit(item); } } } - Ok(functions) } -fn validate_function_local_names( - declaration: &sv_parser::FunctionDeclaration, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Result<(), AnalyzerError> { - let (params, local_types) = match &declaration.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => { - let params = body - .nodes - .3 - .nodes - .1 - .as_ref() - .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) - .unwrap_or_default(); - let local_types = function_local_types_from_block_items( - &body.nodes.5, - syntax_tree, - const_env, - type_aliases, - ) - .unwrap_or_default(); - (params, local_types) - } - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { - let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); - let block_items = body.nodes.4.iter().filter_map(|item| match item { - sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), - sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, - }); - let local_types = function_local_types_from_block_item_iter( - block_items, - syntax_tree, - const_env, - type_aliases, - ) - .unwrap_or_default(); - (params, local_types) - } +fn package_declaration_from_generate_item( + item: &sv_parser::GenerateItem, +) -> Option<&sv_parser::PackageOrGenerateItemDeclaration> { + let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { + return None; }; - if let Some(param) = params - .iter() - .find(|param| local_types.contains_key(¶m.name)) - { - Err(AnalyzerError::Unsupported(format!( - "function local shadows formal `{}`", - param.name - ))) - } else { - Ok(()) - } + let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { + return None; + }; + let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 + else { + return None; + }; + let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = + &**declaration + else { + return None; + }; + Some(declaration) } -fn validate_function_return_type( - declaration: &sv_parser::FunctionDeclaration, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Result<(), AnalyzerError> { - let node = match &declaration.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => &body.nodes.0, - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => &body.nodes.0, +fn generate_item_has_data_declaration(item: &sv_parser::GenerateItem) -> bool { + let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { + return false; }; - let unsupported = match node { - sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => { - let sv_parser::DataTypeOrVoid::DataType(data_type) = &**data_type else { - return Ok(()); - }; - let r#type = type_from_ref_node(RefNode::DataType(data_type), syntax_tree) - .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)); - r#type - .as_ref() - .is_some_and(|r#type| !r#type.unpacked_ranges().is_empty()) - || value_type_from_data_type(data_type, syntax_tree, const_env, type_aliases) - .is_none() - } - sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(_) => false, + let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { + return false; }; - if unsupported { - Err(AnalyzerError::Unsupported( - "unsupported function return data type or unpacked dimension".to_string(), - )) - } else { - Ok(()) - } + let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 + else { + return false; + }; + let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = + &**declaration + else { + return false; + }; + matches!( + &**declaration, + sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(_) + ) } -fn validate_function_formal_types( - declaration: &sv_parser::FunctionDeclaration, +fn next_genvar_value( + value: i128, + iteration: &sv_parser::GenvarIteration, syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, -) -> Result<(), AnalyzerError> { - let unsupported = |node: &sv_parser::DataTypeOrImplicit| { - matches!(node, sv_parser::DataTypeOrImplicit::DataType(_)) - && value_type_from_data_type_or_implicit(node, syntax_tree, const_env, type_aliases) - .is_none() - }; - let has_unpacked_dimensions = - |node: &sv_parser::DataTypeOrImplicit, dimensions: &[sv_parser::VariableDimension]| { - !dimensions.is_empty() - || type_from_ref_node(RefNode::DataTypeOrImplicit(node), syntax_tree) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(node), - syntax_tree, - type_aliases, - ) - }) - .is_some_and(|r#type| !r#type.unpacked_ranges().is_empty()) - }; - let invalid = match &declaration.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => { - body.nodes.3.nodes.1.as_ref().is_some_and(|ports| { - ports - .nodes - .0 - .contents() - .iter() - // A type-only item can be the parser's representation of - // the shorthand `input logic a, b`; only validate entries - // that actually carry a formal identifier here. - .any(|port| { - port.nodes.4.as_ref().is_some_and(|(_, dimensions, _)| { - unsupported(&port.nodes.3) - || has_unpacked_dimensions(&port.nodes.3, dimensions) - }) - }) - }) +) -> Option { + match iteration { + sv_parser::GenvarIteration::Prefix(iteration) => { + let op = syntax_tree.get_str(&iteration.nodes.0.nodes.0.nodes.0)?; + match op { + "++" => value.checked_add(1), + "--" => value.checked_sub(1), + _ => None, + } + } + sv_parser::GenvarIteration::Suffix(iteration) => { + let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0.nodes.0)?; + match op { + "++" => value.checked_add(1), + "--" => value.checked_sub(1), + _ => None, + } + } + sv_parser::GenvarIteration::Assignment(iteration) => { + let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0.nodes.0)?; + let rhs = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&iteration.nodes.2.nodes.0), + syntax_tree, + const_env, + type_aliases, + )?; + let rhs = eval_ast_const_expr(&rhs, const_env)?; + match op { + "=" => Some(rhs), + "+=" => value.checked_add(rhs), + "-=" => value.checked_sub(rhs), + "*=" => value.checked_mul(rhs), + "/=" => (rhs != 0).then(|| value / rhs), + "%=" => (rhs != 0).then(|| value % rhs), + "<<=" => u32::try_from(rhs) + .ok() + .and_then(|rhs| value.checked_shl(rhs)), + ">>=" => u32::try_from(rhs) + .ok() + .and_then(|rhs| value.checked_shr(rhs)), + _ => None, + } } - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => body.nodes.4.iter().any(|item| { - let sv_parser::TfItemDeclaration::TfPortDeclaration(port) = item else { - return false; - }; - unsupported(&port.nodes.3) - || port - .nodes - .4 - .nodes - .0 - .contents() - .iter() - .any(|(_, dimensions, _)| has_unpacked_dimensions(&port.nodes.3, dimensions)) - }), - }; - if invalid { - Err(AnalyzerError::Unsupported( - "unsupported function formal data type or unpacked dimension".to_string(), - )) - } else { - Ok(()) } } -fn validate_function_declaration_statements( - declaration: &sv_parser::FunctionDeclaration, +fn comb_processes_from_generate_block( + block: &sv_parser::GenerateBlock, + condition: Option, syntax_tree: &SyntaxTree, const_env: &HashMap, - type_aliases: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { - let (statements, params, local_types) = match &declaration.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => { - let params = body - .nodes - .3 - .nodes - .1 - .as_ref() - .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) - .unwrap_or_default(); - let local_types = function_local_types_from_block_items( - &body.nodes.5, - syntax_tree, - const_env, - type_aliases, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported function local data type".to_string()) - })?; - (&body.nodes.6, params, local_types) - } - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { - let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); - let block_items = body.nodes.4.iter().filter_map(|item| match item { - sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), - sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, - }); - let local_types = function_local_types_from_block_item_iter( - block_items, + match block { + sv_parser::GenerateBlock::GenerateItem(item) => { + comb_processes_from_generate_item( + item, + condition, syntax_tree, const_env, - type_aliases, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported function local data type".to_string()) - })?; - (&body.nodes.5, params, local_types) + packed_dimensions, + functions, + expression_signedness, + parameter_literals, + processes, + )?; } - }; - let mut assignment_targets = local_types.into_keys().collect::>(); - assignment_targets.extend(params.into_iter().map(|param| param.name)); - for node in RefNode::FunctionDeclaration(declaration) { - let RefNode::BlockingAssignment(assignment) = node else { - continue; - }; - let lhs = match assignment { - sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.0, - sv_parser::BlockingAssignment::OperatorAssignment(assignment) => &assignment.nodes.0, - _ => continue, - }; - let Some(LValue::Ident(name)) = - variable_lvalue_from_node(lhs, syntax_tree, packed_dimensions) - else { - continue; - }; - if !assignment_targets.contains(&name) { - return Err(AnalyzerError::Unsupported(format!( - "function assignment target outside local scope `{name}`" - ))); + sv_parser::GenerateBlock::Multiple(block) => { + let mut block_env = const_env.clone(); + for item in &block.nodes.3 { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + &packed_dimensions.type_aliases, + ) { + continue; + } + comb_processes_from_generate_item( + item, + condition.clone(), + syntax_tree, + &block_env, + packed_dimensions, + functions, + expression_signedness, + parameter_literals, + processes, + )?; + } } } - for statement in statements { - let sv_parser::FunctionStatementOrNull::Statement(statement) = statement else { - continue; - }; - validate_function_statement(&statement.nodes.0, syntax_tree, packed_dimensions)?; - } Ok(()) } -fn validate_function_statement_or_null( - statement: &sv_parser::StatementOrNull, +fn add_localparams_from_generate_item( + item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, -) -> Result<(), AnalyzerError> { - let sv_parser::StatementOrNull::Statement(statement) = statement else { - return Ok(()); - }; - validate_function_statement(statement, syntax_tree, packed_dimensions) + const_env: &mut HashMap, + type_aliases: &HashMap, +) -> bool { + add_localparams_from_generate_item_with_literals( + item, + syntax_tree, + const_env, + type_aliases, + None, + ) } -fn validate_function_statement( - statement: &sv_parser::Statement, +fn add_localparams_from_generate_item_with_literals( + item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, -) -> Result<(), AnalyzerError> { - match &statement.nodes.2 { - sv_parser::StatementItem::JumpStatement(statement) - if matches!(&**statement, sv_parser::JumpStatement::Return(_)) => - { - let sv_parser::JumpStatement::Return(statement) = &**statement else { - unreachable!(); - }; - let expr = statement.nodes.1.as_ref().ok_or_else(|| { - AnalyzerError::Unsupported("expressionless function return".to_string()) - })?; - expr_from_expression_with_types(expr, syntax_tree, packed_dimensions).ok_or_else( - || AnalyzerError::Unsupported("unsupported function return expression".to_string()), - )?; - Ok(()) - } - sv_parser::StatementItem::BlockingAssignment(assignment) => { - let rhs = match &assignment.0 { - sv_parser::BlockingAssignment::Variable(assignment) => &assignment.nodes.3, - sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { - &assignment.nodes.2 - } - _ => { - return Err(AnalyzerError::Unsupported( - "unsupported function assignment".to_string(), - )); - } - }; - expr_from_expression_with_types(rhs, syntax_tree, packed_dimensions).ok_or_else( - || { - AnalyzerError::Unsupported( - "unsupported function assignment expression".to_string(), - ) - }, - )?; - Ok(()) - } - sv_parser::StatementItem::SeqBlock(block) => { - for statement in &block.nodes.3 { - validate_function_statement_or_null(statement, syntax_tree, packed_dimensions)?; - } - Ok(()) - } - sv_parser::StatementItem::ConditionalStatement(statement) => { - expr_from_cond_predicate(&statement.nodes.2.nodes.1, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "unsupported function conditional predicate".to_string(), - ) - })?; - validate_function_statement_or_null( - &statement.nodes.3, - syntax_tree, - packed_dimensions, - )?; - for (_, _, predicate, branch) in &statement.nodes.4 { - expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "unsupported function conditional predicate".to_string(), - ) - })?; - validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; - } - if let Some((_, branch)) = &statement.nodes.5 { - validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; - } - Ok(()) + const_env: &mut HashMap, + type_aliases: &HashMap, + mut parameter_literals: Option<&mut HashMap>, +) -> bool { + let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { + return false; + }; + let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { + return false; + }; + let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 + else { + return false; + }; + let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = + &**declaration + else { + return false; + }; + let sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) = + &**declaration + else { + return false; + }; + let mut parameters = Vec::new(); + if parameters_from_ref_node( + RefNode::LocalParameterDeclaration(&localparam.0), + syntax_tree, + &mut parameters, + true, + const_env, + type_aliases, + &HashMap::default(), + ) + .is_err() + { + return true; + } + let mut parameter_types = parameter_types_from_const_env(const_env); + for parameter in parameters { + let Some(value) = parameter.resolved_value(const_env, ¶meter_types) else { + continue; + }; + let resolved_type = parameter.resolved_type(¶meter_types); + if let Some(r#type) = resolved_type { + parameter_types.insert(parameter.name().to_string(), r#type); + insert_parameter_type_markers(const_env, parameter.name(), r#type); } - sv_parser::StatementItem::CaseStatement(statement) => { - let sv_parser::CaseStatement::Normal(statement) = &**statement else { - return Err(AnalyzerError::Unsupported( - "unsupported statement inside function".to_string(), - )); + if let Some(parameter_literals) = parameter_literals.as_deref_mut() { + let literal = if let Some(r#type) = resolved_type { + format_typed_parameter_literal(value, r#type.width, r#type.signed) + } else { + value.to_string() }; - expr_from_expression_with_types( - &statement.nodes.2.nodes.1.nodes.0, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported function case selector".to_string()) - })?; - for item in std::iter::once(&statement.nodes.3).chain(statement.nodes.4.iter()) { - let branch = match item { - sv_parser::CaseItem::NonDefault(item) => { - for expr in item.nodes.0.contents() { - expr_from_expression_with_types( - &expr.nodes.0, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "unsupported function case item expression".to_string(), - ) - })?; - } - &item.nodes.2 - } - sv_parser::CaseItem::Default(item) => &item.nodes.2, - }; - validate_function_statement_or_null(branch, syntax_tree, packed_dimensions)?; - } - Ok(()) + parameter_literals.insert(parameter.name().to_string(), Expr::Literal(literal)); } - _ => Err(AnalyzerError::Unsupported( - "unsupported statement inside function".to_string(), - )), + const_env.insert(parameter.name().to_string(), value); } + true } -fn function_from_declaration( - declaration: &sv_parser::FunctionDeclaration, - syntax_tree: &SyntaxTree, +fn eval_ast_const_expr(expr: &ConstExpr, const_env: &HashMap) -> Option { + let parameter_types = parameter_types_from_const_env(const_env); + let expr = substitute_typed_parameter_literals(expr.clone(), const_env, ¶meter_types); + typecheck::eval_const_expr(&expr.into(), const_env) +} + +fn substitute_process_constants( + process: CombProcess, const_env: &HashMap, - type_aliases: &HashMap, - packed_dimensions: &PackedDimensions, -) -> Option { - match &declaration.nodes.2 { - sv_parser::FunctionBodyDeclaration::WithPort(body) => { - let name = identifier_text(RefNode::FunctionIdentifier(&body.nodes.2), syntax_tree)?; - let params = body - .nodes - .3 - .nodes - .1 - .as_ref() - .map(|ports| tf_params(ports, syntax_tree, const_env, type_aliases)) - .unwrap_or_default(); - let mut local_types = function_local_types_from_block_items( - &body.nodes.5, - syntax_tree, - const_env, - type_aliases, - )?; - let mut function_packed_dimensions = packed_dimensions.clone(); - function_packed_dimensions.extend(params.iter().map(|param| { - ( - param.name.clone(), - VariableDimensions { - packed: param.packed_dimensions.clone(), - unpacked: Vec::new(), - signed: param.signed, - }, +) -> CombProcess { + substitute_process_constants_with_parameter_literals(process, const_env, &HashMap::default()) +} + +fn substitute_process_constants_with_parameter_literals( + process: CombProcess, + const_env: &HashMap, + parameter_literals: &HashMap, +) -> CombProcess { + CombProcess::new( + process.kind, + process + .condition + .map(|condition| substitute_const_expr_constants(condition, const_env)), + process + .assignments + .into_iter() + .map(|assignment| { + substitute_assignment_constants_with_parameter_literals( + assignment, + const_env, + parameter_literals, ) - })); - function_packed_dimensions.extend(function_local_packed_dimensions_from_block_items( - &body.nodes.5, - syntax_tree, - type_aliases, - )?); - let local_names = local_types.keys().cloned().collect::>(); - insert_function_param_types(¶ms, &mut local_types); - let expr = function_body_expr( - &body.nodes.6, - syntax_tree, - &function_packed_dimensions, - &local_types, - &local_names, - )?; - let return_type = - function_return_type(&body.nodes.0, syntax_tree, const_env, type_aliases); - let return_is_2state = - function_return_is_2state(&body.nodes.0, syntax_tree, type_aliases); - Some(Function { - name, - params, - body: expr, - return_width: return_type.map(|r#type| r#type.width), - return_signed: return_type.is_some_and(|r#type| r#type.signed), - return_is_2state, }) - } - sv_parser::FunctionBodyDeclaration::WithoutPort(body) => { - let name = identifier_text(RefNode::FunctionIdentifier(&body.nodes.2), syntax_tree)?; - let params = tf_item_params(&body.nodes.4, syntax_tree, const_env, type_aliases); - let block_items = body - .nodes - .4 - .iter() - .filter_map(|item| match item { - sv_parser::TfItemDeclaration::BlockItemDeclaration(item) => Some(&**item), - sv_parser::TfItemDeclaration::TfPortDeclaration(_) => None, + .collect(), + ) +} + +fn substitute_assignment_constants( + assignment: Assignment, + const_env: &HashMap, +) -> Assignment { + substitute_assignment_constants_with_parameter_literals( + assignment, + const_env, + &HashMap::default(), + ) +} + +fn substitute_assignment_constants_with_parameter_literals( + assignment: Assignment, + const_env: &HashMap, + parameter_literals: &HashMap, +) -> Assignment { + Assignment::new( + substitute_lvalue_constants(assignment.lhs, const_env), + substitute_expr_constants_with_parameter_literals( + assignment.rhs, + const_env, + parameter_literals, + ), + ) +} + +fn substitute_lvalue_constants(lvalue: LValue, const_env: &HashMap) -> LValue { + match lvalue { + LValue::Ident(name) => LValue::Ident(name), + LValue::Select { + name, + msb, + lsb, + signed, + array_slice_width, + array_slice_reversed, + } => LValue::Select { + name, + msb: substitute_const_expr_constants(msb, const_env), + lsb: substitute_const_expr_constants(lsb, const_env), + signed, + array_slice_width: array_slice_width + .map(|width| substitute_const_expr_constants(width, const_env)), + array_slice_reversed, + }, + } +} + +fn substitute_expr_constants_with_parameter_literals( + expr: Expr, + const_env: &HashMap, + parameter_literals: &HashMap, +) -> Expr { + match expr { + Expr::Ident(name) => parameter_literals + .get(&name) + .cloned() + .or_else(|| { + let value = *const_env.get(&name)?; + if const_env.contains_key(&enum_marker(&name)) { + let width = const_env + .get(¶meter_width_marker(&name)) + .and_then(|width| usize::try_from(*width).ok())?; + let signed = const_env + .get(¶meter_signed_marker(&name)) + .is_some_and(|signed| *signed != 0); + Some(Expr::Literal(format_typed_parameter_literal( + value, width, signed, + ))) + } else if !const_env.contains_key(¶meter_marker(&name)) { + Some(Expr::Literal(value.to_string())) + } else { + None + } + }) + .unwrap_or(Expr::Ident(name)), + Expr::Literal(value) => Expr::Literal(value), + Expr::Select { + expr, + msb, + lsb, + signed, + } => Expr::Select { + expr: Box::new(substitute_expr_constants_with_parameter_literals( + *expr, + const_env, + parameter_literals, + )), + msb: substitute_const_expr_constants(msb, const_env), + lsb: substitute_const_expr_constants(lsb, const_env), + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| { + substitute_expr_constants_with_parameter_literals( + part, + const_env, + parameter_literals, + ) }) - .collect::>(); - let mut local_types = function_local_types_from_block_item_iter( - block_items.iter().copied(), - syntax_tree, + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count: substitute_const_expr_constants(count, const_env), + parts: parts + .into_iter() + .map(|part| { + substitute_expr_constants_with_parameter_literals( + part, + const_env, + parameter_literals, + ) + }) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(substitute_expr_constants_with_parameter_literals( + *expr, + const_env, + parameter_literals, + )), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(substitute_expr_constants_with_parameter_literals( + *expr, const_env, - type_aliases, - )?; - let mut function_packed_dimensions = packed_dimensions.clone(); - function_packed_dimensions.extend(params.iter().map(|param| { - ( - param.name.clone(), - VariableDimensions { - packed: param.packed_dimensions.clone(), - unpacked: Vec::new(), - signed: param.signed, - }, - ) - })); - function_packed_dimensions.extend( - function_local_packed_dimensions_from_block_item_iter( - block_items.iter().copied(), - syntax_tree, - type_aliases, - )?, - ); - let local_names = local_types.keys().cloned().collect::>(); - insert_function_param_types(¶ms, &mut local_types); - let expr = function_body_expr( - &body.nodes.5, - syntax_tree, - &function_packed_dimensions, - &local_types, - &local_names, - )?; - let return_type = - function_return_type(&body.nodes.0, syntax_tree, const_env, type_aliases); - let return_is_2state = - function_return_is_2state(&body.nodes.0, syntax_tree, type_aliases); - Some(Function { - name, - params, - body: expr, - return_width: return_type.map(|r#type| r#type.width), - return_signed: return_type.is_some_and(|r#type| r#type.signed), - return_is_2state, - }) - } + parameter_literals, + )), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(substitute_expr_constants_with_parameter_literals( + *left, + const_env, + parameter_literals, + )), + op, + right: Box::new(substitute_expr_constants_with_parameter_literals( + *right, + const_env, + parameter_literals, + )), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(substitute_expr_constants_with_parameter_literals( + *condition, + const_env, + parameter_literals, + )), + then_expr: Box::new(substitute_expr_constants_with_parameter_literals( + *then_expr, + const_env, + parameter_literals, + )), + else_expr: Box::new(substitute_expr_constants_with_parameter_literals( + *else_expr, + const_env, + parameter_literals, + )), + }, + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| { + substitute_expr_constants_with_parameter_literals( + arg, + const_env, + parameter_literals, + ) + }) + .collect(), + }, } } -fn insert_function_param_types( - params: &[FunctionParam], - local_types: &mut HashMap, -) { - for param in params { - if let Some(width) = param.width { - local_types.insert( - param.name.clone(), - FunctionLocalType { - width, - signed: param.signed, - is_2state: param.is_2state, - }, - ); - } - } +fn expand_process_calls( + process: CombProcess, + functions: &HashMap, + expression_signedness: &HashMap, +) -> CombProcess { + CombProcess::new( + process.kind, + process.condition, + process + .assignments + .into_iter() + .map(|assignment| { + expand_assignment_calls(assignment, functions, expression_signedness, true) + }) + .collect(), + ) } -fn function_local_types_from_block_items( - items: &[sv_parser::BlockItemDeclaration], - syntax_tree: &SyntaxTree, +fn expand_ff_process_calls( + process: FfProcess, + functions: &HashMap, + expression_signedness: &HashMap, const_env: &HashMap, - type_aliases: &HashMap, -) -> Option> { - function_local_types_from_block_item_iter(items.iter(), syntax_tree, const_env, type_aliases) -} - -fn function_local_packed_dimensions_from_block_items( - items: &[sv_parser::BlockItemDeclaration], - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, -) -> Option { - function_local_packed_dimensions_from_block_item_iter(items.iter(), syntax_tree, type_aliases) + parameter_literals: &HashMap, +) -> FfProcess { + FfProcess::new( + process.events, + process + .assignments + .into_iter() + .map(|assignment| { + let condition = assignment.condition.map(|condition| { + substitute_expr_constants_with_parameter_literals( + expand_expr_calls(condition, functions, expression_signedness, 0, true), + const_env, + parameter_literals, + ) + }); + let assignment = substitute_assignment_constants_with_parameter_literals( + expand_assignment_calls( + assignment.assignment, + functions, + expression_signedness, + true, + ), + const_env, + parameter_literals, + ); + ConditionalAssignment::new(condition, assignment) + }) + .collect(), + ) } -fn function_local_packed_dimensions_from_block_item_iter<'a>( - items: impl IntoIterator, - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, -) -> Option { - let mut dimensions = HashMap::default(); - for item in items { - let sv_parser::BlockItemDeclaration::Data(item) = item else { - continue; - }; - let signals = - signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases).ok()?; - dimensions.extend(signals.into_iter().map(|signal| { - ( - signal.name().to_string(), - VariableDimensions { - packed: function_packed_dimension_widths(signal.r#type().packed_ranges()), - unpacked: unpacked_dimension_widths(signal.r#type().unpacked_ranges()), - signed: signal.r#type().is_signed(), - }, - ) - })); - } - Some(dimensions) +fn expand_assignment_calls( + assignment: Assignment, + functions: &HashMap, + expression_signedness: &HashMap, + apply_return_type: bool, +) -> Assignment { + Assignment::new( + expand_lvalue_calls(assignment.lhs, functions, expression_signedness), + expand_expr_calls( + assignment.rhs, + functions, + expression_signedness, + 0, + apply_return_type, + ), + ) } - -fn function_local_types_from_block_item_iter<'a>( - items: impl IntoIterator, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Option> { - let mut local_types = HashMap::default(); - for item in items { - let sv_parser::BlockItemDeclaration::Data(item) = item else { - continue; - }; - let signals = - signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases).ok()?; - for signal in signals { - let r#type = signal.r#type(); - if !r#type.unpacked_ranges().is_empty() { - return None; - } - let width = if r#type.packed_ranges().is_empty() { - 1 - } else { - r#type - .packed_ranges() - .iter() - .try_fold(1usize, |acc, range| { - let left = eval_ast_const_expr(range.left(), const_env)?; - let right = eval_ast_const_expr(range.right(), const_env)?; - acc.checked_mul(left.abs_diff(right) as usize + 1) - })? - }; - local_types.insert( - signal.name().to_string(), - FunctionLocalType { - width, - signed: r#type.is_signed(), - is_2state: r#type.kind() == TypeKind::Bit, - }, - ); - } + +fn expand_lvalue_calls( + lvalue: LValue, + functions: &HashMap, + expression_signedness: &HashMap, +) -> LValue { + let expand_bound = |bound: ConstExpr| { + let original = bound.clone(); + expr_to_lvalue_const(expand_expr_calls( + const_expr_to_expr(bound), + functions, + expression_signedness, + 0, + true, + )) + .unwrap_or(original) + }; + match lvalue { + LValue::Ident(name) => LValue::Ident(name), + LValue::Select { + name, + msb, + lsb, + signed, + array_slice_width, + array_slice_reversed, + } => LValue::Select { + name, + msb: expand_bound(msb), + lsb: expand_bound(lsb), + signed, + array_slice_width: array_slice_width.map(expand_bound), + array_slice_reversed, + }, } - Some(local_types) } -fn function_return_is_2state( - node: &sv_parser::FunctionDataTypeOrImplicit, - syntax_tree: &SyntaxTree, - type_aliases: &HashMap, -) -> bool { - let r#type = match node { - sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { - sv_parser::DataTypeOrVoid::DataType(data_type) => { - type_from_ref_node(RefNode::DataType(data_type), syntax_tree) - .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)) +fn expr_signedness( + expr: &Expr, + identifiers: &HashMap, + functions: &HashMap, +) -> Option { + expr_signedness_with_return_types(expr, identifiers, functions, &HashMap::default()) +} + +fn expr_signedness_with_return_types( + expr: &Expr, + identifiers: &HashMap, + functions: &HashMap, + function_return_types: &HashMap, +) -> Option { + match expr { + Expr::Ident(name) => identifiers.get(name).copied(), + Expr::Literal(literal) => { + typecheck::parse_integral_literal(literal).map(|literal| literal.signed) + } + Expr::Select { signed, .. } => Some(*signed), + Expr::Concat(_) | Expr::RepeatConcat { .. } => Some(false), + Expr::Resize { signed, .. } => Some(*signed), + Expr::Unary { op, expr } => { + if matches!( + op, + UnaryOp::LogicNot | UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor + ) { + Some(false) + } else { + expr_signedness_with_return_types( + expr, + identifiers, + functions, + function_return_types, + ) } - sv_parser::DataTypeOrVoid::Void(_) => None, - }, - sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { - type_from_ref_node(RefNode::ImplicitDataType(data_type), syntax_tree).or_else(|| { - type_alias_from_ref_node( - RefNode::ImplicitDataType(data_type), - syntax_tree, - type_aliases, + } + Expr::Binary { left, op, right } => { + if matches!( + op, + BinaryOp::LogicAnd + | BinaryOp::LogicOr + | BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::EqCase + | BinaryOp::NeCase + | BinaryOp::EqWildcard + | BinaryOp::NeWildcard + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + ) { + Some(false) + } else if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar) { + expr_signedness_with_return_types( + left, + identifiers, + functions, + function_return_types, ) - }) + } else { + Some( + expr_signedness_with_return_types( + left, + identifiers, + functions, + function_return_types, + )? && expr_signedness_with_return_types( + right, + identifiers, + functions, + function_return_types, + )?, + ) + } } - }; - r#type.is_some_and(|r#type| r#type.kind() == TypeKind::Bit) + Expr::Mux { + then_expr, + else_expr, + .. + } => Some( + expr_signedness_with_return_types( + then_expr, + identifiers, + functions, + function_return_types, + )? && expr_signedness_with_return_types( + else_expr, + identifiers, + functions, + function_return_types, + )?, + ), + Expr::Call { name, .. } => functions + .get(name) + .map(|function| function.return_signed) + .or_else(|| { + function_return_types + .get(name) + .map(|metadata| metadata.signed) + }), + } } -fn function_return_type( - node: &sv_parser::FunctionDataTypeOrImplicit, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Option { - match node { - sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { - sv_parser::DataTypeOrVoid::DataType(data_type) => { - value_type_from_data_type(data_type, syntax_tree, const_env, type_aliases) +fn expand_expr_calls( + expr: Expr, + functions: &HashMap, + expression_signedness: &HashMap, + depth: usize, + apply_return_type: bool, +) -> Expr { + if depth > 32 { + return expr; + } + match expr { + Expr::Ident(name) => Expr::Ident(name), + Expr::Literal(value) => Expr::Literal(value), + Expr::Select { + expr, + msb, + lsb, + signed, + } => Expr::Select { + expr: Box::new(expand_expr_calls( + *expr, + functions, + expression_signedness, + depth, + apply_return_type, + )), + msb, + lsb, + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| { + expand_expr_calls( + part, + functions, + expression_signedness, + depth, + apply_return_type, + ) + }) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| { + expand_expr_calls( + part, + functions, + expression_signedness, + depth, + apply_return_type, + ) + }) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(expand_expr_calls( + *expr, + functions, + expression_signedness, + depth, + apply_return_type, + )), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(expand_expr_calls( + *expr, + functions, + expression_signedness, + depth, + apply_return_type, + )), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(expand_expr_calls( + *left, + functions, + expression_signedness, + depth, + apply_return_type, + )), + op, + right: Box::new(expand_expr_calls( + *right, + functions, + expression_signedness, + depth, + apply_return_type, + )), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(expand_expr_calls( + *condition, + functions, + expression_signedness, + depth, + apply_return_type, + )), + then_expr: Box::new(expand_expr_calls( + *then_expr, + functions, + expression_signedness, + depth, + apply_return_type, + )), + else_expr: Box::new(expand_expr_calls( + *else_expr, + functions, + expression_signedness, + depth, + apply_return_type, + )), + }, + Expr::Call { name, args } => { + let args = args + .into_iter() + .map(|arg| { + expand_expr_calls( + arg, + functions, + expression_signedness, + depth, + apply_return_type, + ) + }) + .collect::>(); + let Some(function) = functions.get(&name) else { + return Expr::Call { name, args }; + }; + if function.params.len() != args.len() { + return Expr::Call { name, args }; } - sv_parser::DataTypeOrVoid::Void(_) => None, - }, - sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { - value_type_from_ref_node( - RefNode::ImplicitDataType(data_type), - syntax_tree, - const_env, - type_aliases, - ) - } - } -} - -fn tf_params( - list: &sv_parser::TfPortList, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Vec { - let mut params = Vec::new(); - let mut previous_type = None; - let mut previous_is_2state = false; - let mut previous_packed_dimensions = Vec::new(); - for port in list.nodes.0.contents() { - let type_node = RefNode::DataTypeOrImplicit(&port.nodes.3); - let inferred_type = value_type_from_data_type_or_implicit( - &port.nodes.3, - syntax_tree, - const_env, - type_aliases, - ); - let inferred_is_2state = type_from_ref_node(type_node.clone(), syntax_tree) - .or_else(|| type_alias_from_ref_node(type_node.clone(), syntax_tree, type_aliases)) - .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); - let inferred_packed_dimensions = - packed_dimensions_from_ref_node(type_node.clone(), syntax_tree, type_aliases); - let omitted_type = matches!( - port.nodes.3, - sv_parser::DataTypeOrImplicit::ImplicitDataType(_) - ) && is_signed_from_ref_node(type_node.clone()).is_none() - && packed_ranges_from_ref_node(type_node.clone(), syntax_tree).is_empty(); - let (name, r#type, is_2state, packed_dimensions) = - if let Some((identifier, _, _)) = port.nodes.4.as_ref() { - let Some(name) = identifier_text(RefNode::PortIdentifier(identifier), syntax_tree) - else { - continue; - }; - let r#type = if port.nodes.1.is_none() && omitted_type { - previous_type.or(inferred_type) - } else { - inferred_type - }; - let is_2state = if port.nodes.1.is_none() && omitted_type { - previous_is_2state - } else { - inferred_is_2state - }; - let packed_dimensions = if port.nodes.1.is_none() && omitted_type { - previous_packed_dimensions.clone() + let env = function + .params + .iter() + .zip(args) + .map(|(param, arg)| { + let mut arg = if apply_return_type && let Some(width) = param.width { + let assigned = Expr::Resize { + signed: expr_signedness(&arg, expression_signedness, functions) + .unwrap_or(false), + expr: Box::new(arg), + width, + }; + Expr::Resize { + expr: Box::new(assigned), + width, + signed: param.signed, + } + } else { + arg + }; + if param.is_2state { + arg = Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(arg), + }; + } + (param.name.clone(), arg) + }) + .collect::>(); + let body = substitute_expr_idents(function.body.clone(), &env); + let expanded = expand_expr_calls( + body, + functions, + expression_signedness, + depth + 1, + apply_return_type, + ); + let mut expanded = if apply_return_type && let Some(width) = function.return_width { + let expression_signed = + expr_signedness(&expanded, expression_signedness, functions).unwrap_or(false); + let assigned = if expression_signed == function.return_signed { + expanded } else { - inferred_packed_dimensions + Expr::Resize { + signed: expression_signed, + expr: Box::new(expanded), + width, + } }; - (name, r#type, is_2state, packed_dimensions) - } else { - // An identifier following a comma is syntactically ambiguous with a - // user-defined type. sv-parser represents the shorthand `a, b` as a - // type-only item, so reinterpret an unknown type name as the next - // parameter and inherit the preceding item's type. - if type_alias_from_data_type_or_implicit(&port.nodes.3, syntax_tree, type_aliases) - .is_some() - { - continue; + Expr::Resize { + expr: Box::new(assigned), + width, + signed: function.return_signed, } - let Some(name) = identifier_text(type_node, syntax_tree) else { - continue; - }; - let r#type = if port.nodes.1.is_none() { - previous_type - } else { - Some(ExprType { - width: 1, - signed: false, - }) - }; - let is_2state = port.nodes.1.is_none() && previous_is_2state; - let packed_dimensions = if port.nodes.1.is_none() { - previous_packed_dimensions.clone() - } else { - inferred_packed_dimensions - }; - (name, r#type, is_2state, packed_dimensions) + } else { + expanded }; - previous_type = r#type; - previous_is_2state = is_2state; - previous_packed_dimensions = packed_dimensions.clone(); - params.push(FunctionParam { - name, - width: r#type.map(|r#type| r#type.width), - signed: r#type.is_some_and(|r#type| r#type.signed), - is_2state, - packed_dimensions, - }); + if function.return_is_2state { + expanded = Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(expanded), + }; + } + expanded + } } - params } -fn tf_item_params( - items: &[sv_parser::TfItemDeclaration], - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Vec { - let mut params = Vec::new(); - for item in items { - let sv_parser::TfItemDeclaration::TfPortDeclaration(declaration) = item else { - continue; - }; - let r#type = value_type_from_data_type_or_implicit( - &declaration.nodes.3, - syntax_tree, - const_env, - type_aliases, - ); - let is_2state = type_from_ref_node( - RefNode::DataTypeOrImplicit(&declaration.nodes.3), - syntax_tree, - ) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(&declaration.nodes.3), - syntax_tree, - type_aliases, - ) - }) - .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); - let packed_dimensions = packed_dimensions_from_ref_node( - RefNode::DataTypeOrImplicit(&declaration.nodes.3), - syntax_tree, - type_aliases, - ); - for (identifier, _, _) in declaration.nodes.4.nodes.0.contents() { - let Some(name) = identifier_text(RefNode::PortIdentifier(identifier), syntax_tree) - else { - continue; - }; - params.push(FunctionParam { - name, - width: r#type.map(|r#type| r#type.width), - signed: r#type.is_some_and(|r#type| r#type.signed), - is_2state, - packed_dimensions: packed_dimensions.clone(), - }); - } +fn substitute_expr_idents(expr: Expr, env: &HashMap) -> Expr { + match expr { + Expr::Ident(name) => env.get(&name).cloned().unwrap_or(Expr::Ident(name)), + Expr::Literal(value) => Expr::Literal(value), + Expr::Select { + expr, + msb, + lsb, + signed, + } => Expr::Select { + expr: Box::new(substitute_expr_idents(*expr, env)), + msb, + lsb, + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| substitute_expr_idents(part, env)) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| substitute_expr_idents(part, env)) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(substitute_expr_idents(*expr, env)), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(substitute_expr_idents(*expr, env)), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(substitute_expr_idents(*left, env)), + op, + right: Box::new(substitute_expr_idents(*right, env)), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(substitute_expr_idents(*condition, env)), + then_expr: Box::new(substitute_expr_idents(*then_expr, env)), + else_expr: Box::new(substitute_expr_idents(*else_expr, env)), + }, + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| substitute_expr_idents(arg, env)) + .collect(), + }, } - params } -fn value_type_from_data_type_or_implicit( - node: &sv_parser::DataTypeOrImplicit, - syntax_tree: &SyntaxTree, +fn substitute_const_expr_constants( + expr: ConstExpr, const_env: &HashMap, - type_aliases: &HashMap, -) -> Option { - match node { - sv_parser::DataTypeOrImplicit::DataType(data_type) => { - value_type_from_data_type(data_type, syntax_tree, const_env, type_aliases) - } - sv_parser::DataTypeOrImplicit::ImplicitDataType(data_type) => value_type_from_ref_node( - RefNode::ImplicitDataType(data_type), - syntax_tree, - const_env, - type_aliases, - ), - } +) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, false, false) } -fn value_type_from_data_type( - node: &sv_parser::DataType, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - type_aliases: &HashMap, -) -> Option { - let r#type = type_from_ref_node(RefNode::DataType(node), syntax_tree) - .or_else(|| type_alias_from_data_type(node, syntax_tree, type_aliases))?; - let width = if r#type.packed_ranges().is_empty() { - 1 - } else { - r#type - .packed_ranges() - .iter() - .try_fold(1usize, |acc, range| { - let left = eval_ast_const_expr(range.left(), const_env)?; - let right = eval_ast_const_expr(range.right(), const_env)?; - acc.checked_mul(left.abs_diff(right) as usize + 1) - })? - }; - Some(ExprType { - width, - signed: r#type.is_signed(), - }) +fn substitute_dimension_constants(expr: ConstExpr, const_env: &HashMap) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, true, false) } -fn value_type_from_ref_node( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, +fn substitute_const_expr_constants_preserving_enum_types( + expr: ConstExpr, const_env: &HashMap, - type_aliases: &HashMap, -) -> Option { - let alias = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases); - if alias.is_none() - && let Some(r#type) = integer_atom_expr_type(node.clone()) - { - return Some(r#type); - } - let direct_ranges; - let ranges = if let Some(alias) = &alias { - alias.packed_ranges() - } else { - direct_ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); - &direct_ranges - }; - let width = if ranges.is_empty() { - 1 - } else { - ranges.iter().try_fold(1usize, |acc, range| { - let left = eval_ast_const_expr(range.left(), const_env)?; - let right = eval_ast_const_expr(range.right(), const_env)?; - acc.checked_mul(left.abs_diff(right) as usize + 1) - })? - }; - Some(ExprType { - width, - signed: alias - .as_ref() - .map(|r#type| r#type.is_signed()) - .unwrap_or_else(|| is_signed_from_ref_node(node).unwrap_or(false)), - }) -} - -fn integer_atom_expr_type(node: RefNode<'_>) -> Option { - let atom = unwrap_node!(node.clone(), IntegerAtomType)?; - let RefNode::IntegerAtomType(atom) = atom else { - return None; - }; - let (width, default_signed) = match atom { - sv_parser::IntegerAtomType::Byte(_) => (8, true), - sv_parser::IntegerAtomType::Shortint(_) => (16, true), - sv_parser::IntegerAtomType::Int(_) | sv_parser::IntegerAtomType::Integer(_) => (32, true), - sv_parser::IntegerAtomType::Longint(_) => (64, true), - sv_parser::IntegerAtomType::Time(_) => (64, false), - }; - Some(ExprType { - width, - signed: is_signed_from_ref_node(node).unwrap_or(default_signed), - }) +) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, false, true) } -fn function_body_expr( - statements: &[sv_parser::FunctionStatementOrNull], - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, - local_types: &HashMap, - local_names: &HashSet, -) -> Option { - let mut locals = local_types - .iter() - .map(|(name, r#type)| { - let initial = if local_names.contains(name) { - coerce_function_local_assignment(Expr::Literal("'x".to_string()), *r#type) - } else { - Expr::Ident(name.clone()) +fn substitute_const_expr_constants_impl( + expr: ConstExpr, + const_env: &HashMap, + include_local_parameters: bool, + preserve_enum_types: bool, +) -> ConstExpr { + match expr { + ConstExpr::Ident(name) => { + let Some(value) = const_env.get(&name).filter(|_| { + !const_env.contains_key(¶meter_marker(&name)) + || include_local_parameters + && const_env.contains_key(&local_parameter_marker(&name)) + }) else { + return ConstExpr::Ident(name); }; - (name.clone(), initial) - }) - .collect::>(); - for statement in statements { - if let Some(expr) = function_expr_from_statement_or_null( - statement, - &mut locals, - syntax_tree, - packed_dimensions, - local_types, - ) { - return Some(expr); + if preserve_enum_types && const_env.contains_key(&enum_marker(&name)) { + let width = const_env + .get(¶meter_width_marker(&name)) + .and_then(|width| usize::try_from(*width).ok()); + let signed = const_env + .get(¶meter_signed_marker(&name)) + .is_some_and(|signed| *signed != 0); + if let Some(width) = width { + return ConstExpr::Literal(format_typed_parameter_literal( + *value, width, signed, + )); + } + } + ConstExpr::Literal(value.to_string()) } + ConstExpr::Literal(value) => ConstExpr::Literal(value), + ConstExpr::Select { expr, bit } => ConstExpr::Select { + expr: Box::new(substitute_const_expr_constants_impl( + *expr, + const_env, + include_local_parameters, + preserve_enum_types, + )), + bit: Box::new(substitute_const_expr_constants_impl( + *bit, + const_env, + include_local_parameters, + preserve_enum_types, + )), + }, + ConstExpr::Function { name, args } => ConstExpr::Function { + name, + args: args + .into_iter() + .map(|arg| { + substitute_const_expr_constants_impl( + arg, + const_env, + include_local_parameters, + preserve_enum_types, + ) + }) + .collect(), + }, + ConstExpr::Unary { op, expr } => ConstExpr::Unary { + op, + expr: Box::new(substitute_const_expr_constants_impl( + *expr, + const_env, + include_local_parameters, + preserve_enum_types, + )), + }, + ConstExpr::Binary { left, op, right } => ConstExpr::Binary { + left: Box::new(substitute_const_expr_constants_impl( + *left, + const_env, + include_local_parameters, + preserve_enum_types, + )), + op, + right: Box::new(substitute_const_expr_constants_impl( + *right, + const_env, + include_local_parameters, + preserve_enum_types, + )), + }, + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => ConstExpr::Mux { + condition: Box::new(substitute_const_expr_constants_impl( + *condition, + const_env, + include_local_parameters, + preserve_enum_types, + )), + then_expr: Box::new(substitute_const_expr_constants_impl( + *then_expr, + const_env, + include_local_parameters, + preserve_enum_types, + )), + else_expr: Box::new(substitute_const_expr_constants_impl( + *else_expr, + const_env, + include_local_parameters, + preserve_enum_types, + )), + }, } - None } -fn function_expr_from_statement_or_null( - statement: &sv_parser::FunctionStatementOrNull, - locals: &mut HashMap, +fn assignments_from_continuous_assign( + assign: &sv_parser::ContinuousAssign, syntax_tree: &SyntaxTree, packed_dimensions: &PackedDimensions, - local_types: &HashMap, -) -> Option { - let sv_parser::FunctionStatementOrNull::Statement(statement) = statement else { - return None; - }; - function_expr_from_statement( - &statement.nodes.0, - locals, - syntax_tree, - packed_dimensions, - local_types, - ) +) -> Result, AnalyzerError> { + match assign { + sv_parser::ContinuousAssign::Net(assign) => assign + .nodes + .3 + .nodes + .0 + .contents() + .into_iter() + .map(|assignment| { + let lhs = net_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported("continuous assignment lvalue".to_string()) + })?; + let rhs = expr_from_expression_with_types( + &assignment.nodes.2, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("continuous assignment expression".to_string()) + })?; + Ok(Assignment::new(lhs, rhs)) + }) + .collect(), + sv_parser::ContinuousAssign::Variable(assign) => assign + .nodes + .2 + .nodes + .0 + .contents() + .into_iter() + .map(|assignment| { + let lhs = + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported("continuous assignment lvalue".to_string()) + })?; + let rhs = expr_from_expression_with_types( + &assignment.nodes.2, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("continuous assignment expression".to_string()) + })?; + Ok(Assignment::new(lhs, rhs)) + }) + .collect(), + } } -fn function_expr_from_statement_or_null_stmt( - statement: &sv_parser::StatementOrNull, - locals: &mut HashMap, +fn comb_process_from_always_construct( + always: &sv_parser::AlwaysConstruct, + condition: Option, syntax_tree: &SyntaxTree, packed_dimensions: &PackedDimensions, - local_types: &HashMap, -) -> Option { - let sv_parser::StatementOrNull::Statement(statement) = statement else { - return None; - }; - function_expr_from_statement( - statement, - locals, + functions: &HashMap, + expression_signedness: &HashMap, + parameter_literals: &HashMap, +) -> Result, AnalyzerError> { + if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) { + return Ok(None); + } + validate_always_comb_statement(&always.nodes.1)?; + let mut guarded_assignments = Vec::new(); + conditional_assignments_from_statement( + &always.nodes.1, + None, + true, + true, syntax_tree, + &packed_dimensions.const_env, packed_dimensions, - local_types, - ) + &mut guarded_assignments, + )?; + for guarded in &mut guarded_assignments { + guarded.condition = guarded.condition.take().map(|condition| { + substitute_expr_constants_with_parameter_literals( + expand_expr_calls(condition, functions, expression_signedness, 0, true), + &HashMap::default(), + parameter_literals, + ) + }); + guarded.assignment = substitute_assignment_constants_with_parameter_literals( + expand_assignment_calls( + guarded.assignment.clone(), + functions, + expression_signedness, + true, + ), + &HashMap::default(), + parameter_literals, + ); + } + let assignments = comb_assignments_from_guarded(guarded_assignments, packed_dimensions)?; + Ok((!assignments.is_empty()) + .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) } -fn function_expr_from_statement( - statement: &sv_parser::Statement, - locals: &mut HashMap, - syntax_tree: &SyntaxTree, +/// Merge guarded always_comb assignments into per-target multiplexer chains. +/// +/// Groups made up solely of unconditional writes stay untouched so the +/// downstream ordered-assignment semantics (and its dependency checks) keep +/// applying. Once a conditional write participates, the group folds +/// back-to-front into nested muxes so earlier branches take priority. A +/// conditional write with no later unconditional fallback would keep the +/// previous value, which infers a latch, and is rejected. +fn comb_assignments_from_guarded( + mut guarded: Vec, packed_dimensions: &PackedDimensions, - local_types: &HashMap, -) -> Option { - match &statement.nodes.2 { - sv_parser::StatementItem::JumpStatement(statement) => { - let sv_parser::JumpStatement::Return(statement) = &**statement else { - return None; - }; - let expr = statement.nodes.1.as_ref()?; - expr_from_expression_with_types(expr, syntax_tree, packed_dimensions) - .map(|expr| substitute_expr_idents(expr, locals)) +) -> Result, AnalyzerError> { + validate_dynamic_select_expansion_limits(&guarded, packed_dimensions)?; + normalize_mixed_whole_selected_comb_writes(&mut guarded, packed_dimensions); + let const_env = &packed_dimensions.const_env; + let (mut targets, mut groups) = comb_assignment_target_groups(&guarded, const_env); + coerce_conditional_comb_groups(&mut guarded, &groups, packed_dimensions); + + // Apply every cross-target blocking-assignment substitution before any + // group is materialized. A later group may rewrite an assignment that + // belongs to an earlier group. + let mut lvalues_changed = false; + let mut changed_chains = HashSet::default(); + for (target, indices) in targets.iter().zip(&groups) { + let preserve_target_writes = indices + .iter() + .all(|index| guarded[*index].condition().is_none()); + let initial = overlapping_value_before(&guarded, indices[0], target, packed_dimensions); + let (changed, chains) = substitute_intermediate_comb_value_reads( + &mut guarded, + indices, + target, + initial, + packed_dimensions, + preserve_target_writes, + )?; + lvalues_changed |= changed; + changed_chains.extend(chains); + } + if lvalues_changed { + // A blocking assignment used by a dynamic index can turn one + // syntactic target into different concrete targets on sibling paths. + // Any fallback proof for that chain and the old target grouping are + // stale after the rewrite. + for write in &mut guarded { + if write + .exhaustive_fallback_start + .is_some_and(|chain| changed_chains.contains(&chain)) + { + write.exhaustive_fallback_start = None; + } } - sv_parser::StatementItem::BlockingAssignment(assignment) => { - let (lhs, rhs) = match &assignment.0 { - sv_parser::BlockingAssignment::Variable(assignment) => ( - variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions), - expr_from_expression_with_types( - &assignment.nodes.3, - syntax_tree, - packed_dimensions, - ), - ), - sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { - let lhs = variable_lvalue_from_node( - &assignment.nodes.0, - syntax_tree, - packed_dimensions, - ); - let rhs = expr_from_expression_with_types( - &assignment.nodes.2, - syntax_tree, - packed_dimensions, - ); - let op = syntax_tree.get_str(&assignment.nodes.1.nodes.0.nodes.0); - let rhs = match (&lhs, rhs, op) { - (_, Some(rhs), Some("=")) => Some(rhs), - (Some(lhs), Some(rhs), Some(op)) => { - assignment_op_expr(lhs, op, rhs, packed_dimensions) - } - _ => None, - }; - (lhs, rhs) - } - _ => (None, None), - }; - let Some(LValue::Ident(name)) = lhs else { - return None; - }; - if let Some(rhs) = rhs { - let rhs = substitute_expr_idents(rhs, locals); - let rhs = local_types - .get(&name) - .copied() - .map(|r#type| coerce_function_local_assignment(rhs.clone(), r#type)) - .unwrap_or(rhs); - locals.insert(name, rhs); + (targets, groups) = comb_assignment_target_groups(&guarded, const_env); + coerce_conditional_comb_groups(&mut guarded, &groups, packed_dimensions); + } + + // Merged groups land on the slot of their last write so relative + // statement ordering across different, non-overlapping targets is + // preserved. + let mut slots: Vec> = Vec::with_capacity(guarded.len()); + slots.extend((0..guarded.len()).map(|_| None)); + for (target, indices) in targets.into_iter().zip(groups) { + if indices + .iter() + .all(|index| guarded[*index].condition().is_none()) + { + for index in indices { + slots[index] = Some(guarded[index].assignment().clone()); } - None + continue; } - sv_parser::StatementItem::SeqBlock(block) => { - let mut block_locals = locals.clone(); - for statement in &block.nodes.3 { - if let Some(expr) = function_expr_from_statement_or_null_stmt( - statement, - &mut block_locals, - syntax_tree, - packed_dimensions, - local_types, - ) { - return Some(expr); + let last = *indices.last().expect("group is non-empty"); + // Fold in source order so writes after an if/else retain procedural + // priority. A target-specific exhaustive fallback fills the previous + // value in the branch chain instead of becoming globally + // unconditional. + let initial = overlapping_value_before(&guarded, indices[0], &target, packed_dimensions); + let has_established_initial = initial.is_some(); + let mut current = initial.unwrap_or_else(comb_previous_value_placeholder); + for (position, index) in indices.iter().enumerate() { + let write = &guarded[*index]; + let value = write.assignment().rhs().clone(); + current = if let Some(chain_start) = write.exhaustive_fallback_start { + let mut branch_value = value; + for prior in indices[..position].iter().copied().filter(|prior| { + *prior >= chain_start && guarded[*prior].path_epochs != write.path_epochs + }) { + branch_value = fold_conditional_assignment_over(branch_value, &guarded[prior]); } - } - *locals = block_locals; - None + branch_value + } else { + fold_conditional_assignment_over(current, write) + }; } - sv_parser::StatementItem::ConditionalStatement(statement) => { - function_expr_from_conditional_statement( - statement, - locals, - syntax_tree, - packed_dimensions, - local_types, - ) + let rhs = simplify_constant_mux_conditions(current, const_env); + if expr_contains_comb_previous_value(&rhs) + || (!has_established_initial + && expr_references_overlapping_lvalue(&rhs, &target, const_env)) + { + return Err(AnalyzerError::Unsupported( + "latch inference inside always_comb".to_string(), + )); } - sv_parser::StatementItem::CaseStatement(statement) => function_expr_from_case_statement( - statement, - locals, - syntax_tree, - packed_dimensions, - local_types, - ), - _ => None, + slots[last] = Some(Assignment::new(target, rhs)); } + Ok(slots.into_iter().flatten().collect()) } -fn function_expr_from_conditional_statement( - statement: &sv_parser::ConditionalStatement, - locals: &mut HashMap, - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, - local_types: &HashMap, -) -> Option { - let mut branches = Vec::new(); - let if_condition = - expr_from_cond_predicate(&statement.nodes.2.nodes.1, syntax_tree, packed_dimensions)?; - let mut then_locals = locals.clone(); - let then_expr = function_expr_from_statement_or_null_stmt( - &statement.nodes.3, - &mut then_locals, - syntax_tree, - packed_dimensions, - local_types, - ); - branches.push(( - procedural_truth_condition(substitute_expr_idents(if_condition, locals)), - then_expr, - then_locals, - )); +const MAX_DYNAMIC_SELECT_EXPANSION: u128 = 4_096; - for (_, _, predicate, branch) in &statement.nodes.4 { - let condition = - expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions)?; - let mut branch_locals = locals.clone(); - let branch_expr = function_expr_from_statement_or_null_stmt( - branch, - &mut branch_locals, - syntax_tree, - packed_dimensions, - local_types, - ); - branches.push(( - procedural_truth_condition(substitute_expr_idents(condition, locals)), - branch_expr, - branch_locals, - )); +fn validate_dynamic_select_expansion_limits( + guarded: &[ConditionalAssignment], + packed_dimensions: &PackedDimensions, +) -> Result<(), AnalyzerError> { + for write in guarded { + let LValue::Select { name, msb, lsb, .. } = write.assignment().lhs_value() else { + continue; + }; + if eval_ast_const_expr(msb, &packed_dimensions.const_env).is_some() + && eval_ast_const_expr(lsb, &packed_dimensions.const_env).is_some() + { + continue; + } + let Some(LValue::Select { + msb: whole_msb, + lsb: whole_lsb, + .. + }) = whole_packed_lvalue(name, packed_dimensions) + else { + continue; + }; + let (Some(whole_msb), Some(whole_lsb)) = ( + eval_ast_const_expr(&whole_msb, &packed_dimensions.const_env), + eval_ast_const_expr(&whole_lsb, &packed_dimensions.const_env), + ) else { + continue; + }; + if whole_msb.abs_diff(whole_lsb).saturating_add(1) > MAX_DYNAMIC_SELECT_EXPANSION { + return Err(AnalyzerError::Unsupported( + "dynamic selected write expansion exceeds limit".to_string(), + )); + } } + Ok(()) +} - let (else_expr, else_locals) = if let Some((_, branch)) = &statement.nodes.5 { - let mut branch_locals = locals.clone(); - ( - function_expr_from_statement_or_null_stmt( - branch, - &mut branch_locals, - syntax_tree, - packed_dimensions, - local_types, - ), - branch_locals, - ) - } else { - (None, locals.clone()) - }; - - if branches.iter().all(|(_, expr, _)| expr.is_some()) && else_expr.is_some() { - let mut result = else_expr?; - for (condition, branch_expr, _) in branches.into_iter().rev() { - result = Expr::Mux { - condition: Box::new(condition), - then_expr: Box::new(branch_expr?), - else_expr: Box::new(result), - }; +fn comb_assignment_target_groups( + guarded: &[ConditionalAssignment], + const_env: &HashMap, +) -> (Vec, Vec>) { + let mut targets: Vec = Vec::new(); + let mut groups: Vec> = Vec::new(); + for (index, conditional) in guarded.iter().enumerate() { + let target = conditional.assignment().lhs_value(); + let reusable_group = targets + .iter() + .enumerate() + .rev() + .find_map(|(group, existing)| { + if existing != target { + return None; + } + let previous = *groups[group].last()?; + let separated_by_overlap = guarded[previous + 1..index].iter().any(|assignment| { + lvalues_overlap(assignment.assignment().lhs_value(), target, const_env) + && assignment.assignment().lhs_value() != target + }); + (!separated_by_overlap).then_some(group) + }); + match reusable_group { + Some(group) => groups[group].push(index), + None => { + targets.push(target.clone()); + groups.push(vec![index]); + } } - return Some(result); } + (targets, groups) +} - if branches.iter().all(|(_, expr, _)| expr.is_none()) && else_expr.is_none() { - let mut merged = locals.clone(); - let mut names = locals.keys().cloned().collect::>(); - names.extend(else_locals.keys().cloned()); - names.extend( - branches - .iter() - .flat_map(|(_, _, branch_locals)| branch_locals.keys().cloned()), - ); - for name in names { - let mut value = else_locals - .get(&name) - .or_else(|| locals.get(&name)) - .cloned() - .unwrap_or_else(|| Expr::Ident(name.clone())); - for (condition, _, branch_locals) in branches.iter().rev() { - let branch_value = branch_locals - .get(&name) - .cloned() - .unwrap_or_else(|| value.clone()); - if branch_value != value { - value = Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(branch_value), - else_expr: Box::new(value), - }; +fn coerce_conditional_comb_groups( + guarded: &mut [ConditionalAssignment], + groups: &[Vec], + packed_dimensions: &PackedDimensions, +) { + // Every arm that will participate in a mux must first undergo its own + // procedural assignment conversion. This includes an unconditional + // initializer that becomes the fallback of a later guarded write. + for indices in groups { + let has_conditional = indices + .iter() + .any(|index| guarded[*index].condition().is_some()); + if !has_conditional { + continue; + } + for index in indices { + let assignment = guarded[*index].assignment().clone(); + let lhs = assignment.lhs_value().clone(); + let rhs = + coerce_procedural_assignment_rhs(assignment.rhs().clone(), &lhs, packed_dimensions); + guarded[*index].assignment = Assignment::new(lhs, rhs); + } + } +} + +fn normalize_mixed_whole_selected_comb_writes( + guarded: &mut [ConditionalAssignment], + packed_dimensions: &PackedDimensions, +) { + let mut whole_names = HashSet::default(); + let mut selected_targets: HashMap> = HashMap::default(); + let mut conditional_selected_names = HashSet::default(); + let mut earlier_selected_names = HashSet::default(); + for write in guarded.iter() { + match write.assignment().lhs_value() { + LValue::Ident(name) + if write.condition().is_some() || earlier_selected_names.contains(name) => + { + whole_names.insert(name.clone()); + } + LValue::Ident(_) => {} + target @ LValue::Select { name, .. } => { + selected_targets + .entry(name.clone()) + .or_default() + .push(target.clone()); + if write.condition().is_some() { + conditional_selected_names.insert(name.clone()); } + earlier_selected_names.insert(name.clone()); } - merged.insert(name, value); } - *locals = merged; - return None; + } + whole_names.retain(|name| selected_targets.contains_key(name)); + let mut fallback_targets = HashMap::>::default(); + for write in guarded.iter() { + if let Some(chain_start) = write.exhaustive_fallback_start { + fallback_targets + .entry(chain_start) + .or_default() + .push(write.assignment().lhs_value().clone()); + } + } + let mut normalization_targets = HashMap::default(); + for name in &whole_names { + let Some(whole_target) = whole_packed_lvalue(name, packed_dimensions) else { + continue; + }; + normalization_targets.insert(name.clone(), (whole_target, LValue::Ident(name.clone()))); + } + for name in conditional_selected_names { + let Some(whole_target) = whole_packed_lvalue(&name, packed_dimensions) else { + continue; + }; + if selected_targets + .get(&name) + .is_some_and(|targets| lvalue_is_covered_by(&whole_target, targets, packed_dimensions)) + { + normalization_targets.insert(name.clone(), (whole_target, LValue::Ident(name))); + continue; + } + // Different partitions can exhaustively cover only a subrange of a + // wider declaration. Use the widest written selection that contains + // another partition as their common target. + let Some(targets) = selected_targets.get(&name) else { + continue; + }; + let normalization_target = targets + .iter() + .filter(|candidate| { + targets.iter().any(|target| { + target != *candidate + && lvalue_is_covered_by( + target, + std::slice::from_ref(*candidate), + packed_dimensions, + ) + }) + }) + .filter_map(|candidate| { + let (_, low, high) = lvalue_bit_range(candidate, packed_dimensions)?; + fallback_targets + .values() + .any(|targets| lvalue_is_covered_by(candidate, targets, packed_dimensions)) + .then_some((high.abs_diff(low), candidate.clone())) + }) + .max_by_key(|(width, _)| *width) + .map(|(_, target)| target); + if let Some(target) = normalization_target { + normalization_targets.insert(name, (target.clone(), target)); + } + } + for write in guarded.iter_mut() { + let target = write.assignment().lhs_value().clone(); + let LValue::Select { name, .. } = &target else { + continue; + }; + let Some((normalization_target, assignment_target)) = normalization_targets.get(name) + else { + continue; + }; + if !matches!(assignment_target, LValue::Ident(_)) + && !lvalue_is_covered_by( + &target, + std::slice::from_ref(normalization_target), + packed_dimensions, + ) + { + continue; + } + let write_value = coerce_procedural_assignment_rhs( + write.assignment().rhs().clone(), + &target, + packed_dimensions, + ); + let current = match assignment_target { + LValue::Ident(_) => Expr::Ident(name.clone()), + LValue::Select { .. } => expr_from_lvalue(normalization_target, packed_dimensions), + }; + let Some((rhs, _)) = selected_value_after_write( + ¤t, + normalization_target, + &target, + &write_value, + packed_dimensions, + ) else { + continue; + }; + write.assignment = Assignment::new(assignment_target.clone(), rhs); } - Some(Expr::Call { - name: "$unsupported_mixed_function_conditional".to_string(), - args: Vec::new(), - }) -} - -fn coerce_function_local_assignment(expr: Expr, r#type: FunctionLocalType) -> Expr { - let expr = Expr::Resize { - expr: Box::new(expr), - width: r#type.width, - signed: r#type.signed, - }; - if r#type.is_2state { - Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(expr), + // Several selected writes in one fallback branch become writes to the + // same normalized whole value. Only the final write completes that + // fallback; earlier writes must remain ordinary guarded updates. + let mut last_fallback = HashMap::default(); + for (index, write) in guarded.iter().enumerate() { + if let Some(chain_start) = write.exhaustive_fallback_start { + last_fallback.insert((chain_start, write.assignment().lhs().to_string()), index); + } + } + for (index, write) in guarded.iter_mut().enumerate() { + if let Some(chain_start) = write.exhaustive_fallback_start + && last_fallback.get(&(chain_start, write.assignment().lhs().to_string())) + != Some(&index) + { + write.exhaustive_fallback_start = None; } - } else { - expr } } -fn procedural_truth_condition(condition: Expr) -> Expr { - Expr::Unary { - op: UnaryOp::RedOr, - expr: Box::new(Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(condition), - }), +fn fold_conditional_assignment_over(current: Expr, write: &ConditionalAssignment) -> Expr { + let value = write.assignment().rhs().clone(); + match write.condition() { + None => value, + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(current), + }, } } -fn function_expr_from_case_statement( - statement: &sv_parser::CaseStatement, - locals: &mut HashMap, - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, - local_types: &HashMap, -) -> Option { - let sv_parser::CaseStatement::Normal(statement) = statement else { - return None; - }; - let case_expr = expr_from_expression_with_types( - &statement.nodes.2.nodes.1.nodes.0, - syntax_tree, - packed_dimensions, - )?; - let case_expr = substitute_expr_idents(case_expr, locals); - let mut default_branch = (None, locals.clone()); - let mut branches = Vec::new(); - for item in std::iter::once(&statement.nodes.3).chain(statement.nodes.4.iter()) { - match item { - sv_parser::CaseItem::NonDefault(item) => { - let mut branch_locals = locals.clone(); - let branch_expr = function_expr_from_statement_or_null_stmt( - &item.nodes.2, - &mut branch_locals, - syntax_tree, - packed_dimensions, - local_types, - ); - let conditions = item - .nodes - .0 - .contents() +fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap) -> Expr { + match expr { + Expr::Select { + expr, + msb, + lsb, + signed, + } => Expr::Select { + expr: Box::new(simplify_constant_mux_conditions(*expr, const_env)), + msb, + lsb, + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| simplify_constant_mux_conditions(part, const_env)) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| simplify_constant_mux_conditions(part, const_env)) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => { + let expr = simplify_constant_mux_conditions(*expr, const_env); + if let Some(constant) = expr_to_const(expr.clone()) { + let parameter_types = parameter_types_from_const_env(const_env) .into_iter() - .filter_map(|expr| { - expr_from_expression_with_types( - &expr.nodes.0, - syntax_tree, - packed_dimensions, - ) - }) - .map(|expr| substitute_expr_idents(expr, locals)) - .map(|expr| case_item_condition(case_expr.clone(), expr)) - .collect::>(); - let condition = conditions.into_iter().reduce(|left, right| Expr::Binary { - left: Box::new(left), - op: BinaryOp::LogicOr, - right: Box::new(right), - })?; - branches.push((condition, branch_expr, branch_locals)); + .map(|(name, r#type)| (name, (r#type.width, r#type.signed))) + .collect(); + if let Some(literal) = typecheck::context_size_const_integral_literal( + &crate::ir::ConstExpr::from(constant.clone()), + const_env, + ¶meter_types, + width, + signed, + ) { + return Expr::Literal(typecheck::format_integral_literal_binary(&literal)); + } + if let Some(value) = eval_ast_const_expr(&constant, const_env) { + return Expr::Literal(format_typed_parameter_literal(value, width, signed)); + } } - sv_parser::CaseItem::Default(item) => { - let mut branch_locals = locals.clone(); - default_branch = ( - function_expr_from_statement_or_null_stmt( - &item.nodes.2, - &mut branch_locals, - syntax_tree, - packed_dimensions, - local_types, - ), - branch_locals, - ); + Expr::Resize { + expr: Box::new(expr), + width, + signed, } } - } - - if branches.iter().all(|(_, expr, _)| expr.is_some()) && default_branch.0.is_some() { - let mut result = default_branch.0?; - for (condition, branch_expr, _) in branches.into_iter().rev() { - result = Expr::Mux { - condition: Box::new(condition), - then_expr: Box::new(branch_expr?), - else_expr: Box::new(result), + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(simplify_constant_mux_conditions(*expr, const_env)), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(simplify_constant_mux_conditions(*left, const_env)), + op, + right: Box::new(simplify_constant_mux_conditions(*right, const_env)), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + let condition = simplify_constant_mux_conditions(*condition, const_env); + let mut then_expr = simplify_constant_mux_conditions(*then_expr, const_env); + let mut else_expr = simplify_constant_mux_conditions(*else_expr, const_env); + let parameter_types = parameter_types_from_const_env(const_env); + let arm_type = |arm: &Expr| { + if let Expr::Literal(literal) = arm + && resize_unbased_fill_literal_for_cast(literal, 1, false).is_some() + { + Some(ExprType { + width: 1, + signed: false, + }) + } else if let Expr::Resize { width, signed, .. } = arm { + Some(ExprType { + width: *width, + signed: *signed, + }) + } else { + infer_const_expr_type(&expr_to_const(arm.clone())?, ¶meter_types) + } }; - } - return Some(result); - } - - if branches.iter().all(|(_, expr, _)| expr.is_none()) && default_branch.0.is_none() { - let mut merged = locals.clone(); - let mut names = locals.keys().cloned().collect::>(); - names.extend(default_branch.1.keys().cloned()); - names.extend( - branches - .iter() - .flat_map(|(_, _, branch_locals)| branch_locals.keys().cloned()), - ); - for name in names { - let mut value = default_branch - .1 - .get(&name) - .or_else(|| locals.get(&name)) - .cloned() - .unwrap_or_else(|| Expr::Ident(name.clone())); - for (condition, _, branch_locals) in branches.iter().rev() { - let branch_value = branch_locals - .get(&name) - .cloned() - .unwrap_or_else(|| value.clone()); - if branch_value != value { - value = Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(branch_value), - else_expr: Box::new(value), + let result_type = + arm_type(&then_expr) + .zip(arm_type(&else_expr)) + .map(|(then_type, else_type)| ExprType { + width: then_type.width.max(else_type.width), + signed: then_type.signed && else_type.signed, + }); + // Repeated-condition mux folding is only valid for a condition + // that cannot be X/Z. Procedural guards are explicitly coerced + // to two state, but source-level ternaries need not be. + if expr_is_intrinsically_two_state(&condition, const_env) { + if let Expr::Mux { + condition: nested_condition, + then_expr: nested_then, + .. + } = &then_expr + && **nested_condition == condition + { + then_expr = (**nested_then).clone(); + } + if let Expr::Mux { + condition: nested_condition, + else_expr: nested_else, + .. + } = &else_expr + && **nested_condition == condition + { + else_expr = (**nested_else).clone(); + } + } + if then_expr == else_expr { + return then_expr; + } + if let Some(value) = equivalent_constant_mux_value(&then_expr, &else_expr, const_env) { + return value; + } + match expr_to_const(condition.clone()) + .and_then(|condition| eval_ast_const_expr(&condition, const_env)) + { + Some(value) => { + let selected = if value == 0 { else_expr } else { then_expr }; + let Some(result_type) = result_type else { + return selected; + }; + if let Expr::Literal(literal) = &selected { + if let Some(resized) = resize_unbased_fill_literal_for_cast( + literal, + result_type.width, + result_type.signed, + ) { + return Expr::Literal(resized); + } + } + let Some(selected_type) = arm_type(&selected) else { + return selected; }; + // Both arms determine a ternary's type, even when its + // condition is known. Set that signedness before extending + // the chosen value so an unsigned peer prevents sign extension. + let selected = Expr::Resize { + expr: Box::new(selected), + width: selected_type.width, + signed: result_type.signed, + }; + simplify_constant_mux_conditions( + Expr::Resize { + expr: Box::new(selected), + width: result_type.width, + signed: result_type.signed, + }, + const_env, + ) } + None => Expr::Mux { + condition: Box::new(condition), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + }, } - merged.insert(name, value); } - *locals = merged; - return None; + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| simplify_constant_mux_conditions(arg, const_env)) + .collect(), + }, + Expr::Ident(_) | Expr::Literal(_) => expr, + } +} + +fn fold_const_integral_expr_preserving_mask(expr: Expr, const_env: &HashMap) -> Expr { + // Keep unbased fill literals available for their later comparison context. + // Literal expressions already retain their value, type, and X/Z mask. + if matches!(expr, Expr::Literal(_)) { + return expr; + } + let parameter_types = parameter_types_from_const_env(const_env) + .into_iter() + .map(|(name, r#type)| (name, (r#type.width, r#type.signed))) + .collect(); + eval_const_integral_expr_preserving_mask(&expr, const_env, ¶meter_types) + .map(|literal| Expr::Literal(typecheck::format_integral_literal_binary(&literal))) + .unwrap_or(expr) +} + +fn eval_const_integral_expr_preserving_mask( + expr: &Expr, + const_env: &HashMap, + parameter_types: &HashMap, +) -> Option { + match expr { + Expr::Concat(parts) => concat_integral_literals( + parts + .iter() + .map(|part| { + eval_const_integral_expr_preserving_mask(part, const_env, parameter_types) + }) + .collect::>>()?, + ), + Expr::RepeatConcat { count, parts } => { + let count = usize::try_from(eval_ast_const_expr(count, const_env)?).ok()?; + let part = concat_integral_literals( + parts + .iter() + .map(|part| { + eval_const_integral_expr_preserving_mask(part, const_env, parameter_types) + }) + .collect::>>()?, + )?; + concat_integral_literals(std::iter::repeat_n(part, count)) + } + _ => { + let constant: crate::ir::ConstExpr = expr_to_const(expr.clone())?.into(); + typecheck::eval_const_integral_literal_with_types(&constant, const_env, parameter_types) + } } +} - Some(Expr::Call { - name: "$unsupported_mixed_function_case".to_string(), - args: Vec::new(), +fn concat_integral_literals( + parts: impl IntoIterator, +) -> Option { + let mut width = 0usize; + let mut value = num_bigint::BigUint::default(); + let mut mask = num_bigint::BigUint::default(); + for part in parts { + width = width.checked_add(part.width)?; + value = (value << part.width) | part.value; + mask = (mask << part.width) | part.mask; + } + Some(typecheck::IntegralLiteral { + width, + signed: false, + value, + mask, }) } -fn case_item_condition(case_expr: Expr, item_expr: Expr) -> Expr { - Expr::Binary { - left: Box::new(case_expr), - op: BinaryOp::EqCase, - right: Box::new(item_expr), +fn expr_is_intrinsically_two_state(expr: &Expr, const_env: &HashMap) -> bool { + match expr { + Expr::Ident(name) => const_env.contains_key(name), + Expr::Literal(value) => typecheck::parse_integral_literal(value) + .is_some_and(|literal| literal.mask == num_bigint::BigUint::default()), + Expr::Unary { + op: UnaryOp::ToTwoState, + .. + } => true, + Expr::Unary { expr, .. } => expr_is_intrinsically_two_state(expr, const_env), + Expr::Binary { + op: BinaryOp::EqCase | BinaryOp::NeCase, + .. + } => true, + Expr::Binary { + left, + op: BinaryOp::LogicAnd | BinaryOp::LogicOr, + right, + } => { + expr_is_intrinsically_two_state(left, const_env) + && expr_is_intrinsically_two_state(right, const_env) + } + _ => false, } } -fn comb_processes_from_module_node( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, +fn equivalent_constant_mux_value( + then_expr: &Expr, + else_expr: &Expr, const_env: &HashMap, +) -> Option { + let parameter_types = parameter_types_from_const_env(const_env); + let ir_parameter_types = parameter_types + .iter() + .map(|(name, r#type)| (name.clone(), (r#type.width, r#type.signed))) + .collect::>(); + let then_value = typecheck::eval_const_integral_literal_with_types( + &expr_to_const(then_expr.clone())?.into(), + const_env, + &ir_parameter_types, + )?; + let else_value = typecheck::eval_const_integral_literal_with_types( + &expr_to_const(else_expr.clone())?.into(), + const_env, + &ir_parameter_types, + )?; + let width = then_value.width.max(else_value.width); + let signed = then_value.signed && else_value.signed; + let then_value = resize_integral_literal_for_cast(then_value, width, signed); + let else_value = resize_integral_literal_for_cast(else_value, width, signed); + (then_value == else_value).then_some(Expr::Literal(then_value)) +} + +/// Substitute the value established by earlier writes to `target` into reads +/// that occur before the merged write is emitted. This handles procedural +/// sequences such as `x = 0; y = x; if (c) x = 1;` without making `y` observe +/// the value of `x` from the preceding process activation. +fn substitute_intermediate_comb_value_reads( + guarded: &mut [ConditionalAssignment], + indices: &[usize], + target: &LValue, + initial: Option, packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - let mut processes = Vec::new(); - for item in module_non_port_items(node) { - comb_processes_from_non_port_module_item( - item, - None, - syntax_tree, - const_env, - packed_dimensions, - &mut processes, - )?; + preserve_target_writes: bool, +) -> Result<(bool, HashSet), AnalyzerError> { + let first = *indices.first().expect("group is non-empty"); + let mut initialized = initial.is_some(); + let mut established = initial.unwrap_or_else(comb_previous_value_placeholder); + let mut write_index = 0; + let mut prior_target_writes: Vec<(usize, ConditionalAssignment)> = Vec::new(); + let mut guard_values: HashMap = HashMap::default(); + let mut path_values: HashMap = HashMap::default(); + let mut lvalues_changed = false; + let mut changed_chains = HashSet::default(); + let whole_target = match target { + LValue::Select { name, .. } => whole_packed_lvalue(name, packed_dimensions), + LValue::Ident(_) => None, + }; + let mut whole_established = whole_target.as_ref().and_then(|whole_target| { + overlapping_value_before(guarded, first, whole_target, packed_dimensions) + }); + + // A merged group can be emitted after a later write to one of the values + // used by its original guard or RHS. If that source is not established + // before its first write, there is no expression that can snapshot the + // entry value without introducing hidden process state. + if guarded[..first].iter().any(|assignment| { + assignment.condition().is_some_and(|condition| { + expr_references_overlapping_lvalue(condition, target, &packed_dimensions.const_env) + }) || expr_references_overlapping_lvalue( + assignment.assignment().rhs(), + target, + &packed_dimensions.const_env, + ) + }) { + return Err(AnalyzerError::Unsupported( + "read-before-write dependency inside always_comb".to_string(), + )); } - Ok(processes) -} -fn comb_processes_from_non_port_module_item( - item: &sv_parser::NonPortModuleItem, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::NonPortModuleItem::GenerateRegion(region) => { - for item in ®ion.nodes.1 { - comb_processes_from_generate_item( - item, - condition.clone(), - syntax_tree, - const_env, + for (index, guarded_assignment) in guarded.iter_mut().enumerate().skip(first) { + let is_target_write = indices.get(write_index) == Some(&index); + let original_target_assignment = (is_target_write && preserve_target_writes) + .then(|| guarded_assignment.assignment().clone()); + if let Some(condition) = guarded_assignment.condition.take() { + let condition = if let Some(boundary) = guarded_assignment.guard_boundary { + let value = guard_values + .entry(boundary) + .or_insert_with(|| established.clone()); + if initialized { + substitute_comb_value_reads( + condition, + target, + value, + whole_established.as_ref(), + packed_dimensions, + ) + } else { + condition + } + } else if initialized { + substitute_comb_value_reads( + condition, + target, + &established, + whole_established.as_ref(), packed_dimensions, - processes, - )?; + ) + } else { + condition + }; + guarded_assignment.condition = Some(condition); + } + + let path_value = guarded_assignment + .path_epochs + .iter() + .find_map(|epoch| path_values.get(epoch)); + if initialized || path_value.is_some() { + let value = path_value.unwrap_or(&established); + let assignment = guarded_assignment.assignment.clone(); + let lhs = substitute_comb_lvalue_reads( + assignment.lhs_value().clone(), + target, + value, + whole_established.as_ref(), + packed_dimensions, + ); + if &lhs != assignment.lhs_value() && !(is_target_write && preserve_target_writes) { + lvalues_changed = true; + changed_chains.extend(guarded_assignment.guard_boundary); + changed_chains.extend(guarded_assignment.exhaustive_fallback_start); + } + guarded_assignment.assignment = Assignment::new( + lhs, + substitute_comb_value_reads( + assignment.rhs, + target, + value, + whole_established.as_ref(), + packed_dimensions, + ), + ); + } else if !is_target_write + && (guarded_assignment.condition().is_some_and(|condition| { + expr_references_overlapping_lvalue(condition, target, &packed_dimensions.const_env) + }) || expr_references_overlapping_lvalue( + guarded_assignment.assignment().rhs(), + target, + &packed_dimensions.const_env, + )) + { + return Err(AnalyzerError::Unsupported( + "read-before-write dependency inside always_comb".to_string(), + )); + } + + if !is_target_write { + // Keep the tracked value current across writes to an overlapping + // whole object or subrange. Otherwise a later read could be + // rewritten with the value from before that intervening write. + // Track the value after procedural assignment conversion: an + // unsized RHS such as `wide = 0` has only 32 self-determined bits, + // but subsequent selected writes and reads observe the full LHS. + let write_value = coerce_procedural_assignment_rhs( + guarded_assignment.assignment().rhs().clone(), + guarded_assignment.assignment().lhs_value(), + packed_dimensions, + ); + let tracked_target = match target { + LValue::Ident(name) => whole_packed_lvalue(name, packed_dimensions), + LValue::Select { .. } => Some(target.clone()), + }; + if let Some(tracked_target) = tracked_target + && let Some((updated, covers_target)) = selected_value_after_write( + &established, + &tracked_target, + guarded_assignment.assignment().lhs_value(), + &write_value, + packed_dimensions, + ) + { + established = match guarded_assignment.condition() { + None => { + initialized |= covers_target; + updated + } + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(updated), + else_expr: Box::new(established), + }, + }; } + continue; } - sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { - comb_processes_from_module_or_generate_item( - item, - condition, - syntax_tree, - const_env, + write_index += 1; + let write = &*guarded_assignment; + let value = coerce_procedural_assignment_rhs( + write.assignment().rhs().clone(), + write.assignment().lhs_value(), + packed_dimensions, + ); + let mut tracked_write = write.clone(); + tracked_write.assignment = + Assignment::new(write.assignment().lhs_value().clone(), value.clone()); + if let (Some(whole_target), Some(current_whole)) = + (whole_target.as_ref(), whole_established.clone()) + && let Some((updated, _)) = selected_value_after_write( + ¤t_whole, + whole_target, + target, + &value, packed_dimensions, - processes, - )?; + ) + { + whole_established = Some(match write.condition() { + None => updated, + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(updated), + else_expr: Box::new(current_whole), + }, + }); + } + if let Some(chain_start) = write.exhaustive_fallback_start { + established = value.clone(); + for (prior_index, prior_write) in &prior_target_writes { + if *prior_index >= chain_start + && prior_write.path_epochs != tracked_write.path_epochs + { + established = fold_conditional_assignment_over(established, prior_write); + } + } + initialized = true; + } else { + if write.condition().is_none() { + initialized = true; + } + established = fold_conditional_assignment_over(established, &tracked_write); + } + for (depth, epoch) in tracked_write.path_epochs.iter().enumerate() { + if depth == 0 { + path_values.insert(*epoch, value.clone()); + } else { + let current = path_values + .get(epoch) + .cloned() + .unwrap_or_else(|| established.clone()); + path_values.insert( + *epoch, + fold_conditional_assignment_over(current, &tracked_write), + ); + } + } + prior_target_writes.push((index, tracked_write)); + if let Some(assignment) = original_target_assignment { + guarded_assignment.assignment = assignment; } - _ => {} } - Ok(()) + Ok((lvalues_changed, changed_chains)) } -fn comb_processes_from_generate_item( - item: &sv_parser::GenerateItem, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, +fn substitute_comb_value_reads( + expr: Expr, + target: &LValue, + value: &Expr, + whole_value: Option<&Expr>, packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { - comb_processes_from_module_or_generate_item( - item, - condition, - syntax_tree, - const_env, +) -> Expr { + let substituted = + if let (LValue::Select { name, .. }, Some(whole_value)) = (target, whole_value) { + let mut env = HashMap::default(); + env.insert(name.clone(), whole_value.clone()); + substitute_expr_idents(expr, &env) + } else { + substitute_expr_lvalue(expr, target, value, packed_dimensions) + }; + simplify_single_bit_concat_selects(substituted, packed_dimensions) +} + +fn substitute_comb_lvalue_reads( + lvalue: LValue, + target: &LValue, + value: &Expr, + whole_value: Option<&Expr>, + packed_dimensions: &PackedDimensions, +) -> LValue { + let LValue::Select { + name, + msb, + lsb, + signed, + array_slice_width, + array_slice_reversed, + } = lvalue + else { + return lvalue; + }; + let substitute_bound = |bound: ConstExpr| { + let original = bound.clone(); + expr_to_const(substitute_comb_value_reads( + const_expr_to_expr(bound), + target, + value, + whole_value, packed_dimensions, - processes, - )?; + )) + .unwrap_or(original) + }; + LValue::Select { + name, + msb: substitute_bound(msb), + lsb: substitute_bound(lsb), + signed, + array_slice_width, + array_slice_reversed, } - Ok(()) } -fn comb_processes_from_module_or_generate_item( - item: &sv_parser::ModuleOrGenerateItem, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { - comb_processes_from_module_common_item( - &item.nodes.1, +fn simplify_single_bit_concat_selects(expr: Expr, packed_dimensions: &PackedDimensions) -> Expr { + match expr { + Expr::Select { + expr, + msb, + lsb, + signed, + } => { + let expr = simplify_single_bit_concat_selects(*expr, packed_dimensions); + let bounds = match ( + eval_ast_const_expr(&msb, &packed_dimensions.const_env), + eval_ast_const_expr(&lsb, &packed_dimensions.const_env), + ) { + (Some(msb), Some(lsb)) => Some((msb, lsb)), + _ => None, + }; + if let (Some((msb, lsb)), Expr::Concat(parts)) = (bounds, &expr) + && let (Ok(msb), Ok(lsb)) = (usize::try_from(msb), usize::try_from(lsb)) + && msb >= lsb + { + let mut offset = 0usize; + for part in parts.iter().rev() { + let Some(width) = expr_static_width(part, packed_dimensions) else { + break; + }; + let end = offset.saturating_add(width); + if lsb == offset && msb.checked_add(1) == Some(end) { + return Expr::Resize { + expr: Box::new(part.clone()), + width, + signed: false, + }; + } + if msb == lsb && msb < end { + let selected = msb - offset; + return if width == 1 { + part.clone() + } else { + Expr::Select { + expr: Box::new(part.clone()), + msb: ConstExpr::Literal(selected.to_string()), + lsb: ConstExpr::Literal(selected.to_string()), + signed: false, + } + }; + } + offset = end; + } + } + Expr::Select { + expr: Box::new(expr), + msb, + lsb, + signed, + } + } + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| simplify_single_bit_concat_selects(part, packed_dimensions)) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| simplify_single_bit_concat_selects(part, packed_dimensions)) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(simplify_single_bit_concat_selects(*expr, packed_dimensions)), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(simplify_single_bit_concat_selects(*expr, packed_dimensions)), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(simplify_single_bit_concat_selects(*left, packed_dimensions)), + op, + right: Box::new(simplify_single_bit_concat_selects( + *right, + packed_dimensions, + )), + }, + Expr::Mux { condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(simplify_single_bit_concat_selects( + *condition, + packed_dimensions, + )), + then_expr: Box::new(simplify_single_bit_concat_selects( + *then_expr, + packed_dimensions, + )), + else_expr: Box::new(simplify_single_bit_concat_selects( + *else_expr, + packed_dimensions, + )), + }, + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| simplify_single_bit_concat_selects(arg, packed_dimensions)) + .collect(), + }, + Expr::Ident(_) | Expr::Literal(_) => expr, } - Ok(()) } -fn comb_processes_from_module_common_item( - item: &sv_parser::ModuleCommonItem, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::ModuleCommonItem::ContinuousAssign(assign) => { - processes.extend( - assignments_from_continuous_assign(assign, syntax_tree, packed_dimensions)? - .into_iter() - .map(|assignment| substitute_assignment_constants(assignment, const_env)) - .map(|assignment| { - CombProcess::new( - CombProcessKind::ContinuousAssign, - condition.clone().map(|condition| { - substitute_const_expr_constants(condition, const_env) - }), - vec![assignment], - ) - }), - ); +fn expr_static_width(expr: &Expr, packed_dimensions: &PackedDimensions) -> Option { + match expr { + Expr::Ident(name) => lvalue_expr_type(&LValue::Ident(name.clone()), packed_dimensions) + .map(|r#type| r#type.width) + .or_else(|| variable_size_function_width(&packed_dimensions.const_env, name, false)) + .or_else(|| { + parameter_types_from_const_env(&packed_dimensions.const_env) + .get(name) + .map(|r#type| r#type.width) + }), + Expr::Literal(literal) => { + typecheck::parse_integral_literal(literal).map(|literal| literal.width) } - sv_parser::ModuleCommonItem::AlwaysConstruct(always) => { - if let Some(process) = comb_process_from_always_construct( - always, - condition, - syntax_tree, - packed_dimensions, - )? { - processes.push(substitute_process_constants(process, const_env)); - } + Expr::Select { msb, lsb, .. } => { + let msb = eval_ast_const_expr(msb, &packed_dimensions.const_env)?; + let lsb = eval_ast_const_expr(lsb, &packed_dimensions.const_env)?; + usize::try_from(msb.abs_diff(lsb)).ok()?.checked_add(1) } - sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { - comb_processes_from_conditional_generate( - generate, - condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; + Expr::Concat(parts) => parts.iter().try_fold(0usize, |width, part| { + width.checked_add(expr_static_width(part, packed_dimensions)?) + }), + Expr::RepeatConcat { count, parts } => { + let count = eval_ast_const_expr(count, &packed_dimensions.const_env)?; + let count = usize::try_from(count).ok()?; + let width = parts.iter().try_fold(0usize, |width, part| { + width.checked_add(expr_static_width(part, packed_dimensions)?) + })?; + width.checked_mul(count) } - sv_parser::ModuleCommonItem::LoopGenerateConstruct(generate) => { - comb_processes_from_loop_generate( - generate, - condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; + Expr::Resize { width, .. } => Some(*width), + Expr::Unary { op, expr } => { + if matches!( + op, + UnaryOp::LogicNot | UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor + ) { + Some(1) + } else { + expr_static_width(expr, packed_dimensions) + } } - sv_parser::ModuleCommonItem::NetAlias(_) => { - return Err(AnalyzerError::Unsupported( - "module-level net alias".to_string(), - )); + Expr::Binary { left, op, right } => { + if matches!( + op, + BinaryOp::LogicAnd + | BinaryOp::LogicOr + | BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::EqCase + | BinaryOp::NeCase + | BinaryOp::EqWildcard + | BinaryOp::NeWildcard + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + ) { + Some(1) + } else if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar) { + expr_static_width(left, packed_dimensions) + } else { + Some( + expr_static_width(left, packed_dimensions)? + .max(expr_static_width(right, packed_dimensions)?), + ) + } } - _ => {} + Expr::Mux { + then_expr, + else_expr, + .. + } => Some( + expr_static_width(then_expr, packed_dimensions)? + .max(expr_static_width(else_expr, packed_dimensions)?), + ), + Expr::Call { name, .. } => packed_dimensions + .function_return_types + .get(name) + .and_then(|metadata| metadata.width), } - Ok(()) } -fn comb_processes_from_conditional_generate( - generate: &sv_parser::ConditionalGenerateConstruct, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { - return Ok(()); - }; - let Some(generate_condition) = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1), - syntax_tree, - ) else { - return Err(AnalyzerError::Unsupported( - "conditional-generate condition lowering".to_string(), - )); - }; - if let Some(value) = eval_ast_const_expr(&generate_condition, const_env) { - let block = if value != 0 { - Some(&generate.nodes.2) - } else { - generate.nodes.3.as_ref().map(|(_, block)| block) - }; - if let Some(block) = block { - comb_processes_from_generate_block( - block, - condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; - } - return Ok(()); - } - if has_local_constants(const_env) - && eval_ast_const_expr(&generate_condition, const_env).is_none() - { - return Err(AnalyzerError::Unsupported( - "unknown conditional-generate condition".to_string(), - )); - } - let generate_condition = substitute_const_expr_constants(generate_condition, const_env); - let then_condition = combine_conditions(condition.clone(), generate_condition.clone()); - comb_processes_from_generate_block( - &generate.nodes.2, - then_condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; - if let Some((_, block)) = &generate.nodes.3 { - let else_condition = combine_conditions( - condition, - ConstExpr::Unary { - op: UnaryOp::LogicNot, - expr: Box::new(generate_condition), +fn lvalues_overlap(left: &LValue, right: &LValue, const_env: &HashMap) -> bool { + match (left, right) { + (LValue::Ident(left), LValue::Ident(right)) => left == right, + (LValue::Ident(left), LValue::Select { name: right, .. }) + | (LValue::Select { name: left, .. }, LValue::Ident(right)) => left == right, + ( + LValue::Select { + name: left_name, + msb: left_msb, + lsb: left_lsb, + .. }, - ); - comb_processes_from_generate_block( - block, - else_condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; - } - Ok(()) -} - -fn comb_processes_from_loop_generate( - generate: &sv_parser::LoopGenerateConstruct, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - if generate_block_has_data_declaration(&generate.nodes.2) { - return Ok(()); - } - let name = identifier_text( - RefNode::GenvarIdentifier(&generate.nodes.1.nodes.1.0.nodes.1), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("loop-generate variable".to_string()))?; - let init_expr = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1.0.nodes.3), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("loop-generate initializer".to_string()))?; - let init = eval_ast_const_expr(&init_expr, const_env) - .ok_or_else(|| AnalyzerError::Unsupported("loop-generate initializer".to_string()))?; - let condition_expr = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1.2.nodes.0), - syntax_tree, - ) - .ok_or_else(|| AnalyzerError::Unsupported("loop-generate condition".to_string()))?; - - let mut value = init; - let mut iterations = 0; - for _ in 0..10_000 { - let mut loop_env = const_env.clone(); - loop_env.insert(name.clone(), value); - let condition_value = eval_ast_const_expr(&condition_expr, &loop_env) - .ok_or_else(|| AnalyzerError::Unsupported("loop-generate condition".to_string()))?; - if condition_value == 0 { - break; - } - comb_processes_from_generate_block( - &generate.nodes.2, - condition.clone(), - syntax_tree, - &loop_env, - packed_dimensions, - processes, - )?; - iterations += 1; - let next = next_genvar_value(value, &generate.nodes.1.nodes.1.4, syntax_tree, &loop_env) - .ok_or_else(|| AnalyzerError::Unsupported("genvar update operator".to_string()))?; - value = next; - } - let mut loop_env = const_env.clone(); - loop_env.insert(name, value); - if iterations == 10_000 - && eval_ast_const_expr(&condition_expr, &loop_env).is_some_and(|value| value != 0) - { - return Err(AnalyzerError::Unsupported( - "loop-generate unroll limit exceeded".to_string(), - )); + LValue::Select { + name: right_name, + msb: right_msb, + lsb: right_lsb, + .. + }, + ) => { + if left_name != right_name { + return false; + } + match ( + eval_ast_const_expr(left_msb, const_env), + eval_ast_const_expr(left_lsb, const_env), + eval_ast_const_expr(right_msb, const_env), + eval_ast_const_expr(right_lsb, const_env), + ) { + (Some(left_msb), Some(left_lsb), Some(right_msb), Some(right_lsb)) => { + left_msb.min(left_lsb) <= right_msb.max(right_lsb) + && right_msb.min(right_lsb) <= left_msb.max(left_lsb) + } + _ => true, + } + } } - Ok(()) } -fn generate_block_has_data_declaration(block: &sv_parser::GenerateBlock) -> bool { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => generate_item_has_data_declaration(item), - sv_parser::GenerateBlock::Multiple(block) => { - block.nodes.3.iter().any(generate_item_has_data_declaration) +fn overlapping_value_before( + guarded: &[ConditionalAssignment], + before: usize, + target: &LValue, + packed_dimensions: &PackedDimensions, +) -> Option { + if let LValue::Ident(target_name) = target { + let selected_target = whole_packed_lvalue(target_name, packed_dimensions)?; + return overlapping_value_before(guarded, before, &selected_target, packed_dimensions); + } + let mut current = comb_previous_value_placeholder(); + let mut initialized = false; + let mut states_before = vec![(current.clone(), initialized)]; + for (index, write) in guarded[..before].iter().enumerate() { + let Some((updated, covers_target)) = selected_value_after_write( + ¤t, + target, + write.assignment().lhs_value(), + write.assignment().rhs(), + packed_dimensions, + ) else { + states_before.push((current.clone(), initialized)); + continue; + }; + if let Some(chain_start) = write.exhaustive_fallback_start { + let (base, base_initialized) = states_before + .get(chain_start) + .cloned() + .unwrap_or_else(|| (comb_previous_value_placeholder(), false)); + let Some((mut branch_value, fallback_covers_target)) = selected_value_after_write( + &base, + target, + write.assignment().lhs_value(), + write.assignment().rhs(), + packed_dimensions, + ) else { + states_before.push((current.clone(), initialized)); + continue; + }; + for prior in &guarded[chain_start..index] { + let Some((prior_value, _)) = selected_value_after_write( + &branch_value, + target, + prior.assignment().lhs_value(), + prior.assignment().rhs(), + packed_dimensions, + ) else { + continue; + }; + branch_value = match prior.condition() { + None => prior_value, + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(prior_value), + else_expr: Box::new(branch_value), + }, + }; + } + current = branch_value; + initialized = base_initialized || fallback_covers_target; + } else { + current = match write.condition() { + None => { + initialized |= covers_target; + updated + } + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(updated), + else_expr: Box::new(current), + }, + }; } + states_before.push((current.clone(), initialized)); } + initialized.then_some(current) } -fn generate_block_has_type_declaration(block: &sv_parser::GenerateBlock) -> bool { - RefNode::GenerateBlock(block).into_iter().any(|node| { - matches!( - node, - RefNode::DataDeclaration(sv_parser::DataDeclaration::TypeDeclaration(_)) - | RefNode::TypeAssignment(_) +fn whole_packed_lvalue(name: &str, packed_dimensions: &PackedDimensions) -> Option { + let dimensions = packed_dimensions.get(name)?; + let (msb, lsb) = if dimensions.unpacked.is_empty() + && dimensions.packed.len() == 1 + && !dimensions.packed[0].normalize_single + { + ( + dimensions.packed[0].left.clone(), + dimensions.packed[0].right.clone(), ) + } else { + let width = product_expr( + &dimensions + .packed + .iter() + .map(|dimension| dimension.width.clone()) + .chain( + dimensions + .unpacked + .iter() + .map(|dimension| dimension.width.clone()), + ) + .collect::>(), + ); + ( + ConstExpr::Binary { + left: Box::new(width), + op: BinaryOp::Sub, + right: Box::new(ConstExpr::Literal("1".to_string())), + }, + ConstExpr::Literal("0".to_string()), + ) + }; + Some(LValue::Select { + name: name.to_string(), + msb, + lsb, + signed: dimensions.signed, + array_slice_width: None, + array_slice_reversed: false, }) } -fn has_local_constants(const_env: &HashMap) -> bool { - const_env.keys().any(|name| { - !name.starts_with("__parameter::") && !const_env.contains_key(¶meter_marker(name)) - }) -} - -fn reject_duplicate_conditional_generate_locals( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, -) -> Result<(), AnalyzerError> { - let mut signal_names = HashSet::default(); - let mut parameter_names = HashSet::default(); - for child in node { - let RefNode::ConditionalGenerateConstruct(sv_parser::ConditionalGenerateConstruct::If( - generate, - )) = child - else { - continue; - }; - let blocks = std::iter::once(&generate.nodes.2).chain( - generate - .nodes - .3 - .as_ref() - .into_iter() - .map(|(_, block)| block), - ); - for block in blocks { - for name in generate_block_direct_data_declaration_names(block, syntax_tree) { - if signal_names.insert(name) { - continue; - } - return Err(AnalyzerError::Unsupported( - "local data declaration inside conditional-generate".to_string(), - )); +fn selected_value_after_write( + current: &Expr, + target: &LValue, + write_target: &LValue, + write_value: &Expr, + packed_dimensions: &PackedDimensions, +) -> Option<(Expr, bool)> { + let const_env = &packed_dimensions.const_env; + let LValue::Select { + name: target_name, + msb: target_msb_expr, + lsb: target_lsb_expr, + signed, + .. + } = target + else { + return None; + }; + match write_target { + LValue::Ident(write_name) => (write_name == target_name).then(|| { + let (msb, lsb) = whole_write_select_offsets( + target_name, + target_msb_expr, + target_lsb_expr, + packed_dimensions, + ); + ( + Expr::Select { + expr: Box::new(write_value.clone()), + msb, + lsb, + signed: *signed, + }, + true, + ) + }), + LValue::Select { + name: write_name, + msb: write_msb_expr, + lsb: write_lsb_expr, + .. + } => { + if write_name != target_name { + return None; } - for name in generate_block_direct_local_parameter_names(block, syntax_tree) { - if parameter_names.insert(name) { - continue; + let target_msb = eval_ast_const_expr(target_msb_expr, const_env)?; + let target_lsb = eval_ast_const_expr(target_lsb_expr, const_env)?; + let (write_msb, write_lsb) = match ( + eval_ast_const_expr(write_msb_expr, const_env), + eval_ast_const_expr(write_lsb_expr, const_env), + ) { + (Some(msb), Some(lsb)) => (msb, lsb), + _ => { + return dynamic_selected_value_after_write( + current, + target, + write_target, + write_value, + packed_dimensions, + ); } - return Err(AnalyzerError::Unsupported( - "local data declaration inside conditional-generate".to_string(), - )); + }; + let target_low = target_msb.min(target_lsb); + let target_high = target_msb.max(target_lsb); + let overlap_low = target_low.max(write_msb.min(write_lsb)); + let overlap_high = target_high.min(write_msb.max(write_lsb)); + if overlap_low > overlap_high { + return None; + } + + let target_step = if target_msb >= target_lsb { -1 } else { 1 }; + let overlap_first = if target_step < 0 { + overlap_high + } else { + overlap_low + }; + let overlap_last = if target_step < 0 { + overlap_low + } else { + overlap_high + }; + let mut parts = Vec::new(); + if target_msb != overlap_first { + parts.push(selected_value_read( + current, + target_msb, + target_lsb, + target_msb, + overlap_first.checked_sub(target_step)?, + )?); } + parts.push(selected_value_read( + write_value, + write_msb, + write_lsb, + overlap_first, + overlap_last, + )?); + if overlap_last != target_lsb { + parts.push(selected_value_read( + current, + target_msb, + target_lsb, + overlap_last.checked_add(target_step)?, + target_lsb, + )?); + } + let value = if parts.len() == 1 { + parts.pop()? + } else { + Expr::Concat(parts) + }; + Some(( + value, + overlap_low == target_low && overlap_high == target_high, + )) } } - Ok(()) } -fn conditional_generate_has_leaking_local(module: RefNode<'_>, syntax_tree: &SyntaxTree) -> bool { - let module_identifiers = module - .clone() - .into_iter() - .filter_map(identifier_locate) - .collect::>(); - module.into_iter().any(|node| { - let RefNode::ConditionalGenerateConstruct(sv_parser::ConditionalGenerateConstruct::If( - generate, - )) = node - else { - return false; - }; - std::iter::once(&generate.nodes.2) - .chain( - generate - .nodes - .3 - .as_ref() - .into_iter() - .map(|(_, block)| block), - ) - .any(|block| { - let block_identifiers = RefNode::GenerateBlock(block) - .into_iter() - .filter_map(identifier_locate) - .collect::>(); - let Some(block_start) = block_identifiers.iter().map(|locate| locate.offset).min() - else { - return false; - }; - let block_end = block_identifiers - .iter() - .map(|locate| locate.offset + locate.len) - .max() - .unwrap_or(block_start); - generate_block_direct_data_declaration_names(block, syntax_tree) - .into_iter() - .any(|name| { - module_identifiers.iter().any(|locate| { - (locate.offset < block_start || locate.offset >= block_end) - && syntax_tree.get_str(locate) == Some(name.as_str()) - }) - }) - }) - }) -} +fn dynamic_selected_value_after_write( + current: &Expr, + target: &LValue, + write_target: &LValue, + write_value: &Expr, + packed_dimensions: &PackedDimensions, +) -> Option<(Expr, bool)> { + let LValue::Select { + name, + msb: write_msb, + lsb: write_lsb, + signed, + .. + } = write_target + else { + return None; + }; -fn generate_block_direct_data_declaration_names( - block: &sv_parser::GenerateBlock, - syntax_tree: &SyntaxTree, -) -> Vec { - let aliases = HashMap::default(); - let mut names = Vec::new(); - visit_direct_generate_items(block, |item| { - let Some(sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data)) = - package_declaration_from_generate_item(item) + // Runtime indices still describe a fixed-width selection. Evaluate one + // representative position to recover its direction and width, then use + // case-equality guards so unknown and out-of-range positions are no-ops. + let mut sample_env = packed_dimensions.const_env.clone(); + for variable in packed_dimensions.keys() { + sample_env.entry(variable.clone()).or_insert(0); + } + let sample_msb = eval_ast_const_expr(write_msb, &sample_env)?; + let sample_lsb = eval_ast_const_expr(write_lsb, &sample_env)?; + let delta = sample_msb.checked_sub(sample_lsb)?; + let whole = whole_packed_lvalue(name, packed_dimensions)?; + let LValue::Select { + msb: whole_msb, + lsb: whole_lsb, + .. + } = whole + else { + unreachable!("whole packed lvalue is always a selection"); + }; + let whole_msb = eval_ast_const_expr(&whole_msb, &packed_dimensions.const_env)?; + let whole_lsb = eval_ast_const_expr(&whole_lsb, &packed_dimensions.const_env)?; + let whole_low = whole_msb.min(whole_lsb); + let whole_high = whole_msb.max(whole_lsb); + let candidate_count = whole_high.abs_diff(whole_low).checked_add(1)?; + if candidate_count > MAX_DYNAMIC_SELECT_EXPANSION { + return None; + } + let mut result = current.clone(); + let mut matched = false; + for candidate_lsb in whole_low..=whole_high { + let Some(candidate_msb) = candidate_lsb.checked_add(delta) else { + continue; + }; + if candidate_msb < whole_low || candidate_msb > whole_high { + continue; + } + let candidate = LValue::Select { + name: name.clone(), + msb: const_expr_from_i128(candidate_msb), + lsb: const_expr_from_i128(candidate_lsb), + signed: *signed, + array_slice_width: None, + array_slice_reversed: false, + }; + let Some((updated, _)) = + selected_value_after_write(current, target, &candidate, write_value, packed_dimensions) else { - return; + continue; }; - names.extend( - signals_from_data_declaration(data, syntax_tree, &aliases) - .unwrap_or_default() - .into_iter() - .map(|signal| signal.name), - ); - }); - names + let matches_msb = Expr::Binary { + left: Box::new(const_expr_to_expr(write_msb.clone())), + op: BinaryOp::EqCase, + right: Box::new(const_expr_to_expr(const_expr_from_i128(candidate_msb))), + }; + let matches_lsb = Expr::Binary { + left: Box::new(const_expr_to_expr(write_lsb.clone())), + op: BinaryOp::EqCase, + right: Box::new(const_expr_to_expr(const_expr_from_i128(candidate_lsb))), + }; + result = Expr::Mux { + condition: Box::new(Expr::Binary { + left: Box::new(matches_msb), + op: BinaryOp::LogicAnd, + right: Box::new(matches_lsb), + }), + then_expr: Box::new(updated), + else_expr: Box::new(result), + }; + matched = true; + } + matched.then_some((result, false)) } -fn generate_block_direct_local_parameter_names( - block: &sv_parser::GenerateBlock, - syntax_tree: &SyntaxTree, -) -> Vec { - let mut names = Vec::new(); - visit_direct_generate_items(block, |item| { - let Some(sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration( - localparam, - )) = package_declaration_from_generate_item(item) - else { - return; - }; - let mut parameters = Vec::new(); - if parameters_from_ref_node( - RefNode::LocalParameterDeclaration(&localparam.0), - syntax_tree, - &mut parameters, - true, - ) - .is_ok() - { - names.extend(parameters.into_iter().map(|parameter| parameter.name)); - } - }); - names +fn whole_write_select_offsets( + name: &str, + msb: &ConstExpr, + lsb: &ConstExpr, + packed_dimensions: &PackedDimensions, +) -> (ConstExpr, ConstExpr) { + let Some(dimensions) = packed_dimensions.get(name) else { + return (msb.clone(), lsb.clone()); + }; + if dimensions.unpacked.is_empty() + && dimensions.packed.len() == 1 + && !dimensions.packed[0].normalize_single + { + let dimension = &dimensions.packed[0]; + return ( + packed_index_offset(dimension, msb.clone()), + packed_index_offset(dimension, lsb.clone()), + ); + } + (msb.clone(), lsb.clone()) } -fn visit_direct_generate_items( - block: &sv_parser::GenerateBlock, - mut visit: impl FnMut(&sv_parser::GenerateItem), -) { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => visit(item), - sv_parser::GenerateBlock::Multiple(block) => { - for item in &block.nodes.3 { - visit(item); - } +fn selected_value_read( + value: &Expr, + value_msb: i128, + value_lsb: i128, + first_coordinate: i128, + last_coordinate: i128, +) -> Option { + Some(Expr::Select { + expr: Box::new(value.clone()), + msb: const_expr_from_i128(selected_target_bit(value_msb, value_lsb, first_coordinate)?), + lsb: const_expr_from_i128(selected_target_bit(value_msb, value_lsb, last_coordinate)?), + signed: false, + }) +} + +const COMB_PREVIOUS_VALUE: &str = "\0celox_comb_previous_value"; + +fn comb_previous_value_placeholder() -> Expr { + Expr::Ident(COMB_PREVIOUS_VALUE.to_string()) +} + +fn expr_contains_comb_previous_value(expr: &Expr) -> bool { + match expr { + Expr::Ident(name) => name == COMB_PREVIOUS_VALUE, + Expr::Literal(_) => false, + Expr::Select { expr, .. } | Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { + expr_contains_comb_previous_value(expr) + } + Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => { + parts.iter().any(expr_contains_comb_previous_value) + } + Expr::Binary { left, right, .. } => { + expr_contains_comb_previous_value(left) || expr_contains_comb_previous_value(right) + } + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_contains_comb_previous_value(condition) + || expr_contains_comb_previous_value(then_expr) + || expr_contains_comb_previous_value(else_expr) } + Expr::Call { args, .. } => args.iter().any(expr_contains_comb_previous_value), } } -fn package_declaration_from_generate_item( - item: &sv_parser::GenerateItem, -) -> Option<&sv_parser::PackageOrGenerateItemDeclaration> { - let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { - return None; - }; - let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { +fn substitute_expr_lvalue( + expr: Expr, + target: &LValue, + value: &Expr, + packed_dimensions: &PackedDimensions, +) -> Expr { + if expr_matches_lvalue(&expr, target) { + return value.clone(); + } + if let Some(replacement) = + substitute_overlapping_selected_read(&expr, target, value, packed_dimensions) + { + return replacement; + } + match expr { + Expr::Ident(_) | Expr::Literal(_) => expr, + Expr::Select { + expr, + msb, + lsb, + signed, + } => Expr::Select { + expr: Box::new(substitute_expr_lvalue( + *expr, + target, + value, + packed_dimensions, + )), + msb: substitute_const_expr_lvalue(msb, target, value, packed_dimensions), + lsb: substitute_const_expr_lvalue(lsb, target, value, packed_dimensions), + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| substitute_expr_lvalue(part, target, value, packed_dimensions)) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| substitute_expr_lvalue(part, target, value, packed_dimensions)) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(substitute_expr_lvalue( + *expr, + target, + value, + packed_dimensions, + )), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(substitute_expr_lvalue( + *expr, + target, + value, + packed_dimensions, + )), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(substitute_expr_lvalue( + *left, + target, + value, + packed_dimensions, + )), + op, + right: Box::new(substitute_expr_lvalue( + *right, + target, + value, + packed_dimensions, + )), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(substitute_expr_lvalue( + *condition, + target, + value, + packed_dimensions, + )), + then_expr: Box::new(substitute_expr_lvalue( + *then_expr, + target, + value, + packed_dimensions, + )), + else_expr: Box::new(substitute_expr_lvalue( + *else_expr, + target, + value, + packed_dimensions, + )), + }, + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| substitute_expr_lvalue(arg, target, value, packed_dimensions)) + .collect(), + }, + } +} + +fn substitute_const_expr_lvalue( + expr: ConstExpr, + target: &LValue, + value: &Expr, + packed_dimensions: &PackedDimensions, +) -> ConstExpr { + let original = expr.clone(); + expr_to_const(substitute_expr_lvalue( + const_expr_to_expr(expr), + target, + value, + packed_dimensions, + )) + .unwrap_or(original) +} + +fn substitute_overlapping_selected_read( + expr: &Expr, + target: &LValue, + value: &Expr, + packed_dimensions: &PackedDimensions, +) -> Option { + let const_env = &packed_dimensions.const_env; + let LValue::Select { + name: target_name, + msb: target_msb, + lsb: target_lsb, + .. + } = target + else { return None; }; - let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 + if let Expr::Ident(read_name) = expr { + if read_name != target_name { + return None; + } + let whole = whole_packed_lvalue(read_name, packed_dimensions)?; + return selected_value_after_write( + &Expr::Ident(read_name.clone()), + &whole, + target, + value, + packed_dimensions, + ) + .map(|(value, _)| value); + } + let Expr::Select { + expr: read_base, + msb: read_msb, + lsb: read_lsb, + .. + } = expr else { return None; }; - let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = - &**declaration - else { + let Expr::Ident(read_name) = &**read_base else { return None; }; - Some(declaration) -} + if read_name != target_name { + return None; + } -fn generate_item_has_data_declaration(item: &sv_parser::GenerateItem) -> bool { - let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { - return false; - }; - let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { - return false; - }; - let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 - else { - return false; + let read_msb = eval_ast_const_expr(read_msb, const_env)?; + let read_lsb = eval_ast_const_expr(read_lsb, const_env)?; + let target_msb = eval_ast_const_expr(target_msb, const_env)?; + let target_lsb = eval_ast_const_expr(target_lsb, const_env)?; + let overlap_low = read_msb.min(read_lsb).max(target_msb.min(target_lsb)); + let overlap_high = read_msb.max(read_lsb).min(target_msb.max(target_lsb)); + if overlap_low > overlap_high { + return None; + } + + let read_step = if read_msb >= read_lsb { -1 } else { 1 }; + let overlap_first = if read_step < 0 { + overlap_high + } else { + overlap_low }; - let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = - &**declaration - else { - return false; + let overlap_last = if read_step < 0 { + overlap_low + } else { + overlap_high }; - matches!( - &**declaration, - sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(_) - ) + let mut parts = Vec::new(); + if read_msb != overlap_first { + parts.push(selected_ident_read( + read_name, + read_msb, + overlap_first.checked_sub(read_step)?, + )); + } + + let value_msb = selected_target_bit(target_msb, target_lsb, overlap_first)?; + let value_lsb = selected_target_bit(target_msb, target_lsb, overlap_last)?; + parts.push(Expr::Select { + expr: Box::new(value.clone()), + msb: const_expr_from_i128(value_msb), + lsb: const_expr_from_i128(value_lsb), + signed: false, + }); + + if overlap_last != read_lsb { + parts.push(selected_ident_read( + read_name, + overlap_last.checked_add(read_step)?, + read_lsb, + )); + } + if parts.len() == 1 { + parts.pop() + } else { + Some(Expr::Concat(parts)) + } } -fn next_genvar_value( - value: i128, - iteration: &sv_parser::GenvarIteration, - syntax_tree: &SyntaxTree, +fn selected_target_bit(target_msb: i128, target_lsb: i128, coordinate: i128) -> Option { + if target_msb >= target_lsb { + coordinate.checked_sub(target_lsb) + } else { + target_lsb.checked_sub(coordinate) + } +} + +fn selected_ident_read(name: &str, msb: i128, lsb: i128) -> Expr { + Expr::Select { + expr: Box::new(Expr::Ident(name.to_string())), + msb: const_expr_from_i128(msb), + lsb: const_expr_from_i128(lsb), + signed: false, + } +} + +fn expr_matches_lvalue(expr: &Expr, target: &LValue) -> bool { + match (expr, target) { + (Expr::Ident(expr_name), LValue::Ident(target_name)) => expr_name == target_name, + ( + Expr::Select { expr, msb, lsb, .. }, + LValue::Select { + name, + msb: target_msb, + lsb: target_lsb, + .. + }, + ) => { + matches!(&**expr, Expr::Ident(expr_name) if expr_name == name) + && msb == target_msb + && lsb == target_lsb + } + _ => false, + } +} + +fn expr_references_overlapping_lvalue( + expr: &Expr, + target: &LValue, const_env: &HashMap, -) -> Option { - match iteration { - sv_parser::GenvarIteration::Prefix(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.0.nodes.0)?; - match op { - "++" => value.checked_add(1), - "--" => value.checked_sub(1), - _ => None, +) -> bool { + if expr_matches_lvalue(expr, target) { + return true; + } + match expr { + Expr::Ident(name) => match target { + LValue::Ident(target_name) + | LValue::Select { + name: target_name, .. + } => name == target_name, + }, + Expr::Literal(_) => false, + Expr::Select { + expr: selected, + msb, + lsb, + .. + } => { + let selected_reference = if let Expr::Ident(read_name) = &**selected { + match target { + LValue::Ident(target_name) => read_name == target_name, + LValue::Select { + name: target_name, + msb: target_msb, + lsb: target_lsb, + .. + } if read_name == target_name => match ( + eval_ast_const_expr(msb, const_env), + eval_ast_const_expr(lsb, const_env), + eval_ast_const_expr(target_msb, const_env), + eval_ast_const_expr(target_lsb, const_env), + ) { + (Some(msb), Some(lsb), Some(target_msb), Some(target_lsb)) => { + msb.min(lsb) <= target_msb.max(target_lsb) + && target_msb.min(target_lsb) <= msb.max(lsb) + } + _ => true, + }, + _ => false, + } + } else { + expr_references_overlapping_lvalue(selected, target, const_env) + }; + selected_reference + || expr_references_overlapping_lvalue( + &const_expr_to_expr(msb.clone()), + target, + const_env, + ) + || expr_references_overlapping_lvalue( + &const_expr_to_expr(lsb.clone()), + target, + const_env, + ) + } + Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { + expr_references_overlapping_lvalue(expr, target, const_env) + } + Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts + .iter() + .any(|part| expr_references_overlapping_lvalue(part, target, const_env)), + Expr::Binary { left, right, .. } => { + expr_references_overlapping_lvalue(left, target, const_env) + || expr_references_overlapping_lvalue(right, target, const_env) + } + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_references_overlapping_lvalue(condition, target, const_env) + || expr_references_overlapping_lvalue(then_expr, target, const_env) + || expr_references_overlapping_lvalue(else_expr, target, const_env) + } + Expr::Call { args, .. } => args + .iter() + .any(|arg| expr_references_overlapping_lvalue(arg, target, const_env)), + } +} + +fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), AnalyzerError> { + match &stmt.nodes.2 { + sv_parser::StatementItem::BlockingAssignment(_) => Ok(()), + sv_parser::StatementItem::ConditionalStatement(conditional) => { + validate_always_comb_statement_or_null(&conditional.nodes.3)?; + for (_, _, _, branch) in &conditional.nodes.4 { + validate_always_comb_statement_or_null(branch)?; + } + if let Some((_, branch)) = &conditional.nodes.5 { + validate_always_comb_statement_or_null(branch)?; + } + Ok(()) + } + sv_parser::StatementItem::CaseStatement(case) => { + let sv_parser::CaseStatement::Normal(case) = &**case else { + return Err(AnalyzerError::Unsupported( + "casez, casex, or pattern case inside always_comb".to_string(), + )); + }; + if !matches!(case.nodes.1, sv_parser::CaseKeyword::Case(_)) { + return Err(AnalyzerError::Unsupported( + "casez or casex inside always_comb".to_string(), + )); } - } - sv_parser::GenvarIteration::Suffix(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0)?; - match op { - "++" => value.checked_add(1), - "--" => value.checked_sub(1), - _ => None, + for item in std::iter::once(&case.nodes.3).chain(case.nodes.4.iter()) { + match item { + sv_parser::CaseItem::NonDefault(item) => { + validate_always_comb_statement_or_null(&item.nodes.2)?; + } + sv_parser::CaseItem::Default(item) => { + validate_always_comb_statement_or_null(&item.nodes.2)?; + } + } } + Ok(()) } - sv_parser::GenvarIteration::Assignment(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0)?; - let rhs = const_expr_from_ref_node( - RefNode::ConstantExpression(&iteration.nodes.2.nodes.0), - syntax_tree, - )?; - let rhs = eval_ast_const_expr(&rhs, const_env)?; - match op { - "=" => Some(rhs), - "+=" => value.checked_add(rhs), - "-=" => value.checked_sub(rhs), - "*=" => value.checked_mul(rhs), - "/=" => (rhs != 0).then(|| value / rhs), - "%=" => (rhs != 0).then(|| value % rhs), - "<<=" => u32::try_from(rhs) - .ok() - .and_then(|rhs| value.checked_shl(rhs)), - ">>=" => u32::try_from(rhs) - .ok() - .and_then(|rhs| value.checked_shr(rhs)), - _ => None, + sv_parser::StatementItem::SeqBlock(block) => { + if !block.nodes.2.is_empty() { + return Err(AnalyzerError::Unsupported( + "block-local declaration inside always_comb".to_string(), + )); + } + for stmt in &block.nodes.3 { + if let sv_parser::StatementOrNull::Statement(stmt) = stmt { + validate_always_comb_statement(stmt)?; + } } + Ok(()) } + sv_parser::StatementItem::LoopStatement(loop_statement) => { + let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { + return Err(AnalyzerError::Unsupported( + "unsupported statement inside always_comb".to_string(), + )); + }; + validate_always_comb_statement_or_null(&loop_statement.nodes.2) + } + _ => Err(AnalyzerError::Unsupported( + "unsupported statement inside always_comb".to_string(), + )), } } -fn comb_processes_from_generate_block( - block: &sv_parser::GenerateBlock, - condition: Option, +fn validate_always_comb_statement_or_null( + stmt: &sv_parser::StatementOrNull, +) -> Result<(), AnalyzerError> { + if let sv_parser::StatementOrNull::Statement(stmt) = stmt { + validate_always_comb_statement(stmt)?; + } + Ok(()) +} + +fn ff_processes_from_module_node( + node: RefNode<'_>, syntax_tree: &SyntaxTree, const_env: &HashMap, + parameter_literals: &HashMap, packed_dimensions: &PackedDimensions, - processes: &mut Vec, +) -> Result, AnalyzerError> { + let mut processes = Vec::new(); + for item in module_non_port_items(node) { + ff_processes_from_non_port_module_item( + item, + syntax_tree, + const_env, + parameter_literals, + packed_dimensions, + &mut processes, + )?; + } + Ok(processes) +} + +fn ff_processes_from_non_port_module_item( + item: &sv_parser::NonPortModuleItem, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + parameter_literals: &HashMap, + packed_dimensions: &PackedDimensions, + processes: &mut Vec, ) -> Result<(), AnalyzerError> { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => { - comb_processes_from_generate_item( - item, - condition, - syntax_tree, - const_env, - packed_dimensions, - processes, - )?; - } - sv_parser::GenerateBlock::Multiple(block) => { - let mut block_env = const_env.clone(); - for item in &block.nodes.3 { - if add_localparams_from_generate_item(item, syntax_tree, &mut block_env) { - continue; - } - comb_processes_from_generate_item( + match item { + sv_parser::NonPortModuleItem::GenerateRegion(region) => { + for item in ®ion.nodes.1 { + ff_processes_from_generate_item( item, - condition.clone(), syntax_tree, - &block_env, + const_env, + parameter_literals, packed_dimensions, processes, )?; } } + sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { + ff_processes_from_module_or_generate_item( + item, + syntax_tree, + const_env, + parameter_literals, + packed_dimensions, + processes, + )?; + } + _ => {} } Ok(()) } -fn add_localparams_from_generate_item( - item: &sv_parser::GenerateItem, - syntax_tree: &SyntaxTree, - const_env: &mut HashMap, -) -> bool { - add_localparams_from_generate_item_with_literals(item, syntax_tree, const_env, None) -} - -fn add_localparams_from_generate_item_with_literals( +fn ff_processes_from_generate_item( item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, - const_env: &mut HashMap, - mut parameter_literals: Option<&mut HashMap>, -) -> bool { - let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { - return false; - }; - let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = &**item else { - return false; - }; - let sv_parser::ModuleCommonItem::ModuleOrGenerateItemDeclaration(declaration) = &item.nodes.1 - else { - return false; - }; - let sv_parser::ModuleOrGenerateItemDeclaration::PackageOrGenerateItemDeclaration(declaration) = - &**declaration - else { - return false; - }; - let sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) = - &**declaration - else { - return false; - }; - let mut parameters = Vec::new(); - if parameters_from_ref_node( - RefNode::LocalParameterDeclaration(&localparam.0), - syntax_tree, - &mut parameters, - true, - ) - .is_err() - { - return true; - } - let mut parameter_types = parameter_types_from_const_env(const_env); - for parameter in parameters { - let Some(value) = parameter.resolved_value(const_env, ¶meter_types) else { - continue; - }; - let resolved_type = parameter.resolved_type(¶meter_types); - if let Some(r#type) = resolved_type { - parameter_types.insert(parameter.name().to_string(), r#type); - insert_parameter_type_markers(const_env, parameter.name(), r#type); - } - if let Some(parameter_literals) = parameter_literals.as_deref_mut() { - let literal = if let Some(r#type) = resolved_type { - format_typed_parameter_literal(value, r#type.width, r#type.signed) - } else { - value.to_string() - }; - parameter_literals.insert(parameter.name().to_string(), Expr::Literal(literal)); - } - const_env.insert(parameter.name().to_string(), value); - } - true -} - -fn eval_ast_const_expr(expr: &ConstExpr, const_env: &HashMap) -> Option { - let parameter_types = parameter_types_from_const_env(const_env); - let expr = substitute_typed_parameter_literals(expr.clone(), const_env, ¶meter_types); - typecheck::eval_const_expr(&expr.into(), const_env) -} - -fn substitute_process_constants( - process: CombProcess, - const_env: &HashMap, -) -> CombProcess { - substitute_process_constants_with_parameter_literals(process, const_env, &HashMap::default()) -} - -fn substitute_process_constants_with_parameter_literals( - process: CombProcess, const_env: &HashMap, parameter_literals: &HashMap, -) -> CombProcess { - CombProcess::new( - process.kind, - process - .condition - .map(|condition| substitute_const_expr_constants(condition, const_env)), - process - .assignments - .into_iter() - .map(|assignment| { - substitute_assignment_constants_with_parameter_literals( - assignment, - const_env, - parameter_literals, - ) - }) - .collect(), - ) -} - -fn substitute_assignment_constants( - assignment: Assignment, - const_env: &HashMap, -) -> Assignment { - substitute_assignment_constants_with_parameter_literals( - assignment, - const_env, - &HashMap::default(), - ) + packed_dimensions: &PackedDimensions, + processes: &mut Vec, +) -> Result<(), AnalyzerError> { + if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { + ff_processes_from_module_or_generate_item( + item, + syntax_tree, + const_env, + parameter_literals, + packed_dimensions, + processes, + )?; + } + Ok(()) } -fn substitute_assignment_constants_with_parameter_literals( - assignment: Assignment, +fn ff_processes_from_module_or_generate_item( + item: &sv_parser::ModuleOrGenerateItem, + syntax_tree: &SyntaxTree, const_env: &HashMap, - parameter_literals: &HashMap, -) -> Assignment { - Assignment::new( - substitute_lvalue_constants(assignment.lhs, const_env), - substitute_expr_constants_with_parameter_literals( - assignment.rhs, + parameter_literals: &HashMap, + packed_dimensions: &PackedDimensions, + processes: &mut Vec, +) -> Result<(), AnalyzerError> { + if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { + ff_processes_from_module_common_item( + &item.nodes.1, + syntax_tree, const_env, parameter_literals, - ), - ) -} - -fn substitute_lvalue_constants(lvalue: LValue, const_env: &HashMap) -> LValue { - match lvalue { - LValue::Ident(name) => LValue::Ident(name), - LValue::Select { - name, - msb, - lsb, - signed, - array_slice_width, - array_slice_reversed, - } => LValue::Select { - name, - msb: substitute_const_expr_constants(msb, const_env), - lsb: substitute_const_expr_constants(lsb, const_env), - signed, - array_slice_width: array_slice_width - .map(|width| substitute_const_expr_constants(width, const_env)), - array_slice_reversed, - }, + packed_dimensions, + processes, + )?; } + Ok(()) } -fn substitute_expr_constants_with_parameter_literals( - expr: Expr, +fn ff_processes_from_module_common_item( + item: &sv_parser::ModuleCommonItem, + syntax_tree: &SyntaxTree, const_env: &HashMap, parameter_literals: &HashMap, -) -> Expr { - match expr { - Expr::Ident(name) => parameter_literals - .get(&name) - .cloned() - .or_else(|| { - const_env - .get(&name) - .filter(|_| !const_env.contains_key(¶meter_marker(&name))) - .map(|value| Expr::Literal(value.to_string())) - }) - .unwrap_or(Expr::Ident(name)), - Expr::Literal(value) => Expr::Literal(value), - Expr::Select { - expr, - msb, - lsb, - signed, - } => Expr::Select { - expr: Box::new(substitute_expr_constants_with_parameter_literals( - *expr, - const_env, - parameter_literals, - )), - msb: substitute_const_expr_constants(msb, const_env), - lsb: substitute_const_expr_constants(lsb, const_env), - signed, - }, - Expr::Concat(parts) => Expr::Concat( - parts - .into_iter() - .map(|part| { - substitute_expr_constants_with_parameter_literals( - part, - const_env, - parameter_literals, - ) - }) - .collect(), - ), - Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { - count: substitute_const_expr_constants(count, const_env), - parts: parts - .into_iter() - .map(|part| { - substitute_expr_constants_with_parameter_literals( - part, - const_env, - parameter_literals, - ) - }) - .collect(), - }, - Expr::Resize { - expr, - width, - signed, - } => Expr::Resize { - expr: Box::new(substitute_expr_constants_with_parameter_literals( - *expr, - const_env, - parameter_literals, - )), - width, - signed, - }, - Expr::Unary { op, expr } => Expr::Unary { - op, - expr: Box::new(substitute_expr_constants_with_parameter_literals( - *expr, - const_env, - parameter_literals, - )), - }, - Expr::Binary { left, op, right } => Expr::Binary { - left: Box::new(substitute_expr_constants_with_parameter_literals( - *left, - const_env, - parameter_literals, - )), - op, - right: Box::new(substitute_expr_constants_with_parameter_literals( - *right, - const_env, - parameter_literals, - )), - }, - Expr::Mux { - condition, - then_expr, - else_expr, - } => Expr::Mux { - condition: Box::new(substitute_expr_constants_with_parameter_literals( - *condition, + packed_dimensions: &PackedDimensions, + processes: &mut Vec, +) -> Result<(), AnalyzerError> { + match item { + sv_parser::ModuleCommonItem::AlwaysConstruct(always) => { + if let Some(process) = ff_process_from_always_construct( + always, + syntax_tree, const_env, parameter_literals, - )), - then_expr: Box::new(substitute_expr_constants_with_parameter_literals( - *then_expr, + packed_dimensions, + )? { + processes.push(process); + } + } + sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { + let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { + return Ok(()); + }; + let Some(condition) = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(&generate.nodes.1.nodes.1), + syntax_tree, const_env, - parameter_literals, - )), - else_expr: Box::new(substitute_expr_constants_with_parameter_literals( - *else_expr, + &packed_dimensions.type_aliases, + ) else { + return Err(AnalyzerError::Unsupported( + "unknown conditional-generate condition".to_string(), + )); + }; + let Some(condition_value) = eval_ast_const_expr(&condition, const_env) else { + return Err(AnalyzerError::Unsupported( + "unknown conditional-generate condition".to_string(), + )); + }; + let block = if condition_value != 0 { + Some(&generate.nodes.2) + } else { + generate.nodes.3.as_ref().map(|(_, block)| block) + }; + if let Some(block) = block { + ff_processes_from_generate_block( + block, + syntax_tree, + const_env, + parameter_literals, + packed_dimensions, + processes, + )?; + } + } + _ => {} + } + Ok(()) +} + +fn ff_processes_from_generate_block( + block: &sv_parser::GenerateBlock, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + parameter_literals: &HashMap, + packed_dimensions: &PackedDimensions, + processes: &mut Vec, +) -> Result<(), AnalyzerError> { + match block { + sv_parser::GenerateBlock::GenerateItem(item) => { + ff_processes_from_generate_item( + item, + syntax_tree, const_env, parameter_literals, - )), - }, - Expr::Call { name, args } => Expr::Call { - name, - args: args - .into_iter() - .map(|arg| { + packed_dimensions, + processes, + )?; + } + sv_parser::GenerateBlock::Multiple(block) => { + let mut block_env = const_env.clone(); + let mut block_parameter_literals = parameter_literals.clone(); + for item in &block.nodes.3 { + if add_localparams_from_generate_item_with_literals( + item, + syntax_tree, + &mut block_env, + &packed_dimensions.type_aliases, + Some(&mut block_parameter_literals), + ) { + continue; + } + ff_processes_from_generate_item( + item, + syntax_tree, + &block_env, + &block_parameter_literals, + packed_dimensions, + processes, + )?; + } + } + } + Ok(()) +} + +fn ff_process_from_always_construct( + always: &sv_parser::AlwaysConstruct, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + parameter_literals: &HashMap, + packed_dimensions: &PackedDimensions, +) -> Result, AnalyzerError> { + if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) { + return Ok(None); + } + let Some((events, body)) = ff_event_control_and_body(&always.nodes.1, syntax_tree) else { + return Err(AnalyzerError::Unsupported( + "always_ff event expression".to_string(), + )); + }; + let mut assignments = Vec::new(); + conditional_assignments_from_statement_or_null( + body, + None, + false, + false, + syntax_tree, + const_env, + packed_dimensions, + &mut assignments, + )?; + let assignments = assignments + .into_iter() + .map(|assignment| { + ConditionalAssignment::new( + assignment.condition.map(|condition| { substitute_expr_constants_with_parameter_literals( - arg, + condition, const_env, parameter_literals, ) - }) - .collect(), - }, + }), + substitute_assignment_constants_with_parameter_literals( + assignment.assignment, + const_env, + parameter_literals, + ), + ) + }) + .collect::>(); + Ok( + (!events.is_empty() && !assignments.is_empty()) + .then(|| FfProcess::new(events, assignments)), + ) +} + +fn ff_event_control_and_body<'a>( + stmt: &'a sv_parser::Statement, + syntax_tree: &SyntaxTree, +) -> Option<(Vec, &'a sv_parser::StatementOrNull)> { + let sv_parser::StatementItem::ProceduralTimingControlStatement(timing) = &stmt.nodes.2 else { + return None; + }; + let sv_parser::ProceduralTimingControl::EventControl(event_control) = &timing.nodes.0 else { + return None; + }; + Some(( + ff_events_from_event_control(event_control, syntax_tree)?, + &timing.nodes.1, + )) +} + +fn ff_events_from_event_control( + control: &sv_parser::EventControl, + syntax_tree: &SyntaxTree, +) -> Option> { + let sv_parser::EventControl::EventExpression(control) = control else { + return None; + }; + ff_events_from_event_expression(&control.nodes.1.nodes.1, syntax_tree) +} + +fn ff_events_from_event_expression( + expr: &sv_parser::EventExpression, + syntax_tree: &SyntaxTree, +) -> Option> { + match expr { + sv_parser::EventExpression::Expression(expr) => { + let edge = match expr.nodes.0.as_ref()? { + sv_parser::EdgeIdentifier::Posedge(_) => FfEdge::Pos, + sv_parser::EdgeIdentifier::Negedge(_) => FfEdge::Neg, + sv_parser::EdgeIdentifier::Edge(_) => return None, + }; + let signal = expr_ident_name(&expr_from_expression(&expr.nodes.1, syntax_tree)?); + signal.map(|signal| vec![FfEvent::new(edge, signal)]) + } + sv_parser::EventExpression::Or(expr) => { + let mut left = ff_events_from_event_expression(&expr.nodes.0, syntax_tree)?; + left.extend(ff_events_from_event_expression(&expr.nodes.2, syntax_tree)?); + Some(left) + } + sv_parser::EventExpression::Comma(expr) => { + let mut left = ff_events_from_event_expression(&expr.nodes.0, syntax_tree)?; + left.extend(ff_events_from_event_expression(&expr.nodes.2, syntax_tree)?); + Some(left) + } + sv_parser::EventExpression::Paren(expr) => { + ff_events_from_event_expression(&expr.nodes.0.nodes.1, syntax_tree) + } + _ => None, + } +} + +fn conditional_assignments_from_statement_or_null( + stmt: &sv_parser::StatementOrNull, + condition: Option, + exhaustive_fallback: bool, + retain_unreachable_writes: bool, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + assignments: &mut Vec, +) -> Result<(), AnalyzerError> { + if let sv_parser::StatementOrNull::Statement(stmt) = stmt { + conditional_assignments_from_statement( + stmt, + condition, + exhaustive_fallback, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + } + Ok(()) +} + +fn conditional_assignments_from_statement( + stmt: &sv_parser::Statement, + condition: Option, + exhaustive_fallback: bool, + retain_unreachable_writes: bool, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + assignments: &mut Vec, +) -> Result<(), AnalyzerError> { + match &stmt.nodes.2 { + sv_parser::StatementItem::BlockingAssignment(assignment) => { + let lowered = match &assignment.0 { + sv_parser::BlockingAssignment::Variable(assignment) => { + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + .zip(expr_from_expression_with_types( + &assignment.nodes.3, + syntax_tree, + packed_dimensions, + )) + } + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + let op = syntax_tree.get_str(&assignment.nodes.1.nodes.0.nodes.0); + let lhs = variable_lvalue_from_node( + &assignment.nodes.0, + syntax_tree, + packed_dimensions, + ); + let rhs = expr_from_expression_with_types( + &assignment.nodes.2, + syntax_tree, + packed_dimensions, + ); + match (lhs, rhs, op) { + (Some(lhs), Some(rhs), Some("=")) => Some((lhs, rhs)), + (Some(lhs), Some(rhs), Some(op)) => { + assignment_op_expr(&lhs, op, rhs.clone(), packed_dimensions) + .map(|rhs| (lhs, rhs)) + } + _ => None, + } + } + _ => None, + }; + let Some((lhs, rhs)) = lowered else { + return Err(AnalyzerError::Unsupported( + "always_comb assignment expression".to_string(), + )); + }; + let rhs = if condition.is_some() { + coerce_procedural_assignment_rhs(rhs, &lhs, packed_dimensions) + } else { + rhs + }; + assignments.push(ConditionalAssignment::new( + condition, + Assignment::new(lhs, rhs), + )); + } + sv_parser::StatementItem::NonblockingAssignment(assignment) => { + let lhs = + variable_lvalue_from_node(&assignment.0.nodes.0, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported("always_ff assignment lowering".to_string()) + })?; + let rhs = expr_from_expression_with_types( + &assignment.0.nodes.3, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported("always_ff assignment lowering".to_string()) + })?; + let rhs = if condition.is_some() { + coerce_procedural_assignment_rhs(rhs, &lhs, packed_dimensions) + } else { + rhs + }; + assignments.push(ConditionalAssignment::new( + condition, + Assignment::new(lhs, rhs), + )); + } + sv_parser::StatementItem::SeqBlock(block) => { + for stmt in &block.nodes.3 { + conditional_assignments_from_statement_or_null( + stmt, + condition.clone(), + exhaustive_fallback, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + } + } + sv_parser::StatementItem::ConditionalStatement(stmt) => { + conditional_assignments_from_conditional_statement( + stmt, + condition, + exhaustive_fallback, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + } + sv_parser::StatementItem::CaseStatement(stmt) => { + conditional_assignments_from_case_statement( + stmt, + condition, + exhaustive_fallback, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + } + sv_parser::StatementItem::LoopStatement(loop_statement) => { + let (name, values) = static_for_loop_iterations(loop_statement, syntax_tree, const_env) + .ok_or_else(|| { + AnalyzerError::Unsupported("unsupported procedural for loop".to_string()) + })?; + let body = match &**loop_statement { + sv_parser::LoopStatement::For(loop_statement) => &loop_statement.nodes.2, + _ => unreachable!(), + }; + let iterations = if values.is_empty() && retain_unreachable_writes { + let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { + unreachable!(); + }; + let (_, initial_value) = + static_for_loop_initial_value(loop_statement, syntax_tree, const_env) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "unsupported procedural for loop".to_string(), + ) + })?; + vec![(initial_value, false)] + } else { + values.into_iter().map(|value| (value, true)).collect() + }; + for (value, reachable) in iterations { + let mut loop_env = const_env.clone(); + loop_env.insert(name.clone(), value); + let mut loop_packed_dimensions = packed_dimensions.clone(); + let mut loop_const_env = loop_env.clone(); + insert_parameter_type_markers( + &mut loop_const_env, + &name, + ExprType { + width: 32, + signed: true, + }, + ); + loop_packed_dimensions.const_env = loop_const_env.clone(); + let start = assignments.len(); + let iteration_condition = if reachable { + condition.clone() + } else { + combine_expr_conditions(condition.clone(), Expr::Literal("1'b0".to_string())) + }; + conditional_assignments_from_statement_or_null( + body, + iteration_condition, + exhaustive_fallback && reachable, + retain_unreachable_writes, + syntax_tree, + &loop_const_env, + &loop_packed_dimensions, + assignments, + )?; + for assignment in &mut assignments[start..] { + assignment.condition = assignment.condition.take().map(|condition| { + substitute_expr_constants_with_parameter_literals( + condition, + &loop_env, + &HashMap::default(), + ) + }); + assignment.assignment = + substitute_assignment_constants(assignment.assignment.clone(), &loop_env); + } + } + } + _ => { + return Err(AnalyzerError::Unsupported( + "unsupported statement inside always_ff".to_string(), + )); + } } + Ok(()) } -fn expand_process_calls( - process: CombProcess, - functions: &HashMap, - expression_signedness: &HashMap, -) -> CombProcess { - CombProcess::new( - process.kind, - process.condition, - process - .assignments - .into_iter() - .map(|assignment| { - expand_assignment_calls(assignment, functions, expression_signedness, true) +fn coerce_procedural_assignment_rhs( + rhs: Expr, + lhs: &LValue, + packed_dimensions: &PackedDimensions, +) -> Expr { + let Some(target_type) = lvalue_expr_type(lhs, packed_dimensions) else { + return rhs; + }; + let identifier_signedness = packed_dimensions + .iter() + .map(|(name, dimensions)| (name.clone(), dimensions.signed)) + .collect(); + let Some(source_signed) = expr_signedness_with_return_types( + &rhs, + &identifier_signedness, + &HashMap::default(), + &packed_dimensions.function_return_types, + ) else { + return rhs; + }; + let assigned = if source_signed == target_type.signed + && expr_static_width(&rhs, packed_dimensions) == Some(target_type.width) + { + rhs + } else { + let assigned = Expr::Resize { + expr: Box::new(rhs), + width: target_type.width, + signed: source_signed, + }; + Expr::Resize { + expr: Box::new(assigned), + width: target_type.width, + signed: target_type.signed, + } + }; + let name = match lhs { + LValue::Ident(name) | LValue::Select { name, .. } => name, + }; + if packed_dimensions + .get(name) + .is_some_and(|dimensions| dimensions.is_2state) + { + Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(assigned), + } + } else { + assigned + } +} + +fn lvalue_expr_type(value: &LValue, packed_dimensions: &PackedDimensions) -> Option { + match value { + LValue::Ident(name) => { + let dimensions = packed_dimensions.get(name)?; + let width = dimensions + .packed + .iter() + .map(|dimension| &dimension.width) + .chain(dimensions.unpacked.iter().map(|dimension| &dimension.width)) + .try_fold(1usize, |width, dimension| { + let dimension_width = + eval_ast_const_expr(dimension, &packed_dimensions.const_env)?; + width.checked_mul(usize::try_from(dimension_width).ok()?) + })?; + Some(ExprType { + width: width.max(1), + signed: dimensions.signed, }) - .collect(), - ) + } + LValue::Select { + msb, lsb, signed, .. + } => { + let msb = eval_ast_const_expr(msb, &packed_dimensions.const_env)?; + let lsb = eval_ast_const_expr(lsb, &packed_dimensions.const_env)?; + Some(ExprType { + width: usize::try_from(msb.abs_diff(lsb)).ok()?.checked_add(1)?, + signed: *signed, + }) + } + } } -fn expand_ff_process_calls( - process: FfProcess, - functions: &HashMap, - expression_signedness: &HashMap, +fn conditional_assignments_from_conditional_statement( + stmt: &sv_parser::ConditionalStatement, + parent_condition: Option, + exhaustive_fallback: bool, + retain_unreachable_writes: bool, + syntax_tree: &SyntaxTree, const_env: &HashMap, - parameter_literals: &HashMap, -) -> FfProcess { - FfProcess::new( - process.events, - process - .assignments - .into_iter() - .map(|assignment| { - let condition = assignment.condition.map(|condition| { - substitute_expr_constants_with_parameter_literals( - expand_expr_calls(condition, functions, expression_signedness, 0, true), - const_env, - parameter_literals, - ) - }); - let assignment = substitute_assignment_constants_with_parameter_literals( - expand_assignment_calls( - assignment.assignment, - functions, - expression_signedness, - true, - ), - const_env, - parameter_literals, - ); - ConditionalAssignment::new(condition, assignment) - }) - .collect(), - ) + packed_dimensions: &PackedDimensions, + assignments: &mut Vec, +) -> Result<(), AnalyzerError> { + let chain_start = assignments.len(); + let if_condition = + expr_from_cond_predicate(&stmt.nodes.2.nodes.1, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) + })?; + let mut prior_false = Vec::new(); + let mut definitely_assigned_branches = Vec::new(); + let then_condition = combine_expr_conditions( + parent_condition.clone(), + procedural_truth_condition(if_condition.clone()), + ); + let then_start = assignments.len(); + conditional_assignments_from_statement_or_null( + &stmt.nodes.3, + then_condition, + false, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + mark_condition_context(assignments, then_start, chain_start); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + &stmt.nodes.3, + syntax_tree, + packed_dimensions, + )); + prior_false.push(procedural_false_condition(if_condition)); + + for (_, _, predicate, branch) in &stmt.nodes.4 { + let branch_condition = + expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions) + .ok_or_else(|| { + AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) + })?; + let mut terms = prior_false.clone(); + terms.push(procedural_truth_condition(branch_condition.clone())); + let condition = combine_expr_condition_terms(parent_condition.clone(), terms); + let branch_start = assignments.len(); + conditional_assignments_from_statement_or_null( + branch, + condition, + false, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + mark_condition_context(assignments, branch_start, chain_start); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); + prior_false.push(procedural_false_condition(branch_condition)); + } + + if let Some((_, branch)) = &stmt.nodes.5 { + // Keep the false-path guard while lowering. It can only be removed + // for targets that are written in every branch of this chain. + let exhaustive = exhaustive_fallback && parent_condition.is_none(); + let condition = combine_expr_condition_terms(parent_condition, prior_false); + let branch_start = assignments.len(); + conditional_assignments_from_statement_or_null( + branch, + condition.clone(), + false, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + mark_condition_context(assignments, branch_start, chain_start); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); + if exhaustive { + mark_exhaustive_fallback( + &mut assignments[branch_start..], + &definitely_assigned_branches, + chain_start, + packed_dimensions, + ); + } + } + Ok(()) } -fn expand_assignment_calls( - assignment: Assignment, - functions: &HashMap, - expression_signedness: &HashMap, - apply_return_type: bool, -) -> Assignment { - Assignment::new( - assignment.lhs, - expand_expr_calls( - assignment.rhs, - functions, - expression_signedness, - 0, - apply_return_type, - ), - ) +fn procedural_false_condition(condition: Expr) -> Expr { + Expr::Unary { + op: UnaryOp::LogicNot, + expr: Box::new(Expr::Unary { + op: UnaryOp::ToTwoState, + expr: Box::new(condition), + }), + } } -fn expr_signedness( - expr: &Expr, - identifiers: &HashMap, - functions: &HashMap, -) -> Option { - match expr { - Expr::Ident(name) => identifiers.get(name).copied(), - Expr::Literal(literal) => { - typecheck::parse_integral_literal(literal).map(|literal| literal.signed) +fn conditional_assignments_from_case_statement( + stmt: &sv_parser::CaseStatement, + parent_condition: Option, + exhaustive_fallback: bool, + retain_unreachable_writes: bool, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, + assignments: &mut Vec, +) -> Result<(), AnalyzerError> { + let chain_start = assignments.len(); + let sv_parser::CaseStatement::Normal(stmt) = stmt else { + return Err(AnalyzerError::Unsupported( + "casez, casex, or pattern case inside always_comb".to_string(), + )); + }; + if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { + return Err(AnalyzerError::Unsupported( + "casez or casex inside always_comb".to_string(), + )); + } + let case_expr = expr_from_expression_with_types( + &stmt.nodes.2.nodes.1.nodes.0, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| AnalyzerError::Unsupported("always_ff case selector lowering".to_string()))?; + let item_reachability = + two_state_case_item_reachability(stmt, syntax_tree, const_env, packed_dimensions); + let complete_two_state_case = item_reachability + .as_ref() + .is_some_and(|(_, covered)| *covered); + let preserve_unmatched_writes = + item_reachability + .as_ref() + .is_some_and(|(reachable, covered)| { + !*covered && !reachable.iter().any(|reachable| *reachable) + }); + + let mut branches = Vec::new(); + let mut default_branch = None; + for (item_index, item) in std::iter::once(&stmt.nodes.3) + .chain(stmt.nodes.4.iter()) + .enumerate() + { + if item_reachability + .as_ref() + .is_some_and(|(reachable, _)| !reachable[item_index]) + && !preserve_unmatched_writes + { + continue; } - Expr::Select { signed, .. } => Some(*signed), - Expr::Concat(_) | Expr::RepeatConcat { .. } => Some(false), - Expr::Resize { signed, .. } => Some(*signed), - Expr::Unary { op, expr } => { - if matches!( - op, - UnaryOp::LogicNot | UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor - ) { - Some(false) - } else { - expr_signedness(expr, identifiers, functions) + match item { + sv_parser::CaseItem::NonDefault(item) => { + let mut conditions = Vec::new(); + for expr in item.nodes.0.contents() { + let expr = expr_from_expression_with_types( + &expr.nodes.0, + syntax_tree, + packed_dimensions, + ) + .ok_or_else(|| { + AnalyzerError::Unsupported( + "always_ff case item expression lowering".to_string(), + ) + })?; + conditions.push(case_item_condition(case_expr.clone(), expr)); + } + if let Some(condition) = conditions.into_iter().reduce(|left, right| Expr::Binary { + left: Box::new(left), + op: BinaryOp::LogicOr, + right: Box::new(right), + }) { + branches.push((condition, &item.nodes.2)); + } } - } - Expr::Binary { left, op, right } => { - if matches!( - op, - BinaryOp::LogicAnd - | BinaryOp::LogicOr - | BinaryOp::Eq - | BinaryOp::Ne - | BinaryOp::EqCase - | BinaryOp::NeCase - | BinaryOp::EqWildcard - | BinaryOp::NeWildcard - | BinaryOp::Lt - | BinaryOp::Le - | BinaryOp::Gt - | BinaryOp::Ge - ) { - Some(false) - } else if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar) { - expr_signedness(left, identifiers, functions) - } else { - Some( - expr_signedness(left, identifiers, functions)? - && expr_signedness(right, identifiers, functions)?, - ) + sv_parser::CaseItem::Default(item) => { + default_branch = Some(&item.nodes.2); } } - Expr::Mux { - then_expr, - else_expr, - .. - } => Some( - expr_signedness(then_expr, identifiers, functions)? - && expr_signedness(else_expr, identifiers, functions)?, - ), - Expr::Call { name, .. } => functions.get(name).map(|function| function.return_signed), } -} -fn expand_expr_calls( - expr: Expr, - functions: &HashMap, - expression_signedness: &HashMap, - depth: usize, - apply_return_type: bool, -) -> Expr { - if depth > 32 { - return expr; - } - match expr { - Expr::Ident(name) => Expr::Ident(name), - Expr::Literal(value) => Expr::Literal(value), - Expr::Select { - expr, - msb, - lsb, - signed, - } => Expr::Select { - expr: Box::new(expand_expr_calls( - *expr, - functions, - expression_signedness, - depth, - apply_return_type, - )), - msb, - lsb, - signed, - }, - Expr::Concat(parts) => Expr::Concat( - parts - .into_iter() - .map(|part| { - expand_expr_calls( - part, - functions, - expression_signedness, - depth, - apply_return_type, - ) - }) - .collect(), - ), - Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { - count, - parts: parts - .into_iter() - .map(|part| { - expand_expr_calls( - part, - functions, - expression_signedness, - depth, - apply_return_type, - ) - }) - .collect(), - }, - Expr::Resize { - expr, - width, - signed, - } => Expr::Resize { - expr: Box::new(expand_expr_calls( - *expr, - functions, - expression_signedness, - depth, - apply_return_type, - )), - width, - signed, - }, - Expr::Unary { op, expr } => Expr::Unary { - op, - expr: Box::new(expand_expr_calls( - *expr, - functions, - expression_signedness, - depth, - apply_return_type, - )), - }, - Expr::Binary { left, op, right } => Expr::Binary { - left: Box::new(expand_expr_calls( - *left, - functions, - expression_signedness, - depth, - apply_return_type, - )), - op, - right: Box::new(expand_expr_calls( - *right, - functions, - expression_signedness, - depth, - apply_return_type, - )), - }, - Expr::Mux { + let mut prior_false = Vec::new(); + let mut definitely_assigned_branches = Vec::new(); + let branch_count = branches.len(); + for (branch_index, (branch_condition, branch)) in branches.into_iter().enumerate() { + let mut terms = prior_false.clone(); + terms.push(branch_condition.clone()); + let condition = combine_expr_condition_terms(parent_condition.clone(), terms); + // Case-item guards are never tautological, so statements nested in + // a branch must keep the item condition. + let branch_start = assignments.len(); + conditional_assignments_from_statement_or_null( + branch, condition, - then_expr, - else_expr, - } => Expr::Mux { - condition: Box::new(expand_expr_calls( - *condition, - functions, - expression_signedness, - depth, - apply_return_type, - )), - then_expr: Box::new(expand_expr_calls( - *then_expr, - functions, - expression_signedness, - depth, - apply_return_type, - )), - else_expr: Box::new(expand_expr_calls( - *else_expr, - functions, - expression_signedness, - depth, - apply_return_type, - )), - }, - Expr::Call { name, args } => { - let args = args - .into_iter() - .map(|arg| { - expand_expr_calls( - arg, - functions, - expression_signedness, - depth, - apply_return_type, - ) - }) - .collect::>(); - let Some(function) = functions.get(&name) else { - return Expr::Call { name, args }; - }; - if function.params.len() != args.len() { - return Expr::Call { name, args }; - } - let env = function - .params - .iter() - .zip(args) - .map(|(param, arg)| { - let mut arg = if apply_return_type && let Some(width) = param.width { - let assigned = Expr::Resize { - signed: expr_signedness(&arg, expression_signedness, functions) - .unwrap_or(false), - expr: Box::new(arg), - width, - }; - Expr::Resize { - expr: Box::new(assigned), - width, - signed: param.signed, - } - } else { - arg - }; - if param.is_2state { - arg = Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(arg), - }; - } - (param.name.clone(), arg) - }) - .collect::>(); - let body = substitute_expr_idents(function.body.clone(), &env); - let expanded = expand_expr_calls( - body, - functions, - expression_signedness, - depth + 1, - apply_return_type, - ); - let mut expanded = if apply_return_type && let Some(width) = function.return_width { - let expression_signed = - expr_signedness(&expanded, expression_signedness, functions).unwrap_or(false); - let assigned = if expression_signed == function.return_signed { - expanded - } else { - Expr::Resize { - signed: expression_signed, - expr: Box::new(expanded), - width, - } - }; - Expr::Resize { - expr: Box::new(assigned), - width, - signed: function.return_signed, - } - } else { - expanded - }; - if function.return_is_2state { - expanded = Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(expanded), - }; - } - expanded + false, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + mark_condition_context(assignments, branch_start, chain_start); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); + if complete_two_state_case + && exhaustive_fallback + && parent_condition.is_none() + && branch_index + 1 == branch_count + { + mark_exhaustive_fallback( + &mut assignments[branch_start..], + &definitely_assigned_branches, + chain_start, + packed_dimensions, + ); } + prior_false.push(Expr::Unary { + op: UnaryOp::LogicNot, + expr: Box::new(branch_condition), + }); } -} -fn substitute_expr_idents(expr: Expr, env: &HashMap) -> Expr { - match expr { - Expr::Ident(name) => env.get(&name).cloned().unwrap_or(Expr::Ident(name)), - Expr::Literal(value) => Expr::Literal(value), - Expr::Select { - expr, - msb, - lsb, - signed, - } => Expr::Select { - expr: Box::new(substitute_expr_idents(*expr, env)), - msb, - lsb, - signed, - }, - Expr::Concat(parts) => Expr::Concat( - parts - .into_iter() - .map(|part| substitute_expr_idents(part, env)) - .collect(), - ), - Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { - count, - parts: parts - .into_iter() - .map(|part| substitute_expr_idents(part, env)) - .collect(), - }, - Expr::Resize { - expr, - width, - signed, - } => Expr::Resize { - expr: Box::new(substitute_expr_idents(*expr, env)), - width, - signed, - }, - Expr::Unary { op, expr } => Expr::Unary { - op, - expr: Box::new(substitute_expr_idents(*expr, env)), - }, - Expr::Binary { left, op, right } => Expr::Binary { - left: Box::new(substitute_expr_idents(*left, env)), - op, - right: Box::new(substitute_expr_idents(*right, env)), - }, - Expr::Mux { - condition, - then_expr, - else_expr, - } => Expr::Mux { - condition: Box::new(substitute_expr_idents(*condition, env)), - then_expr: Box::new(substitute_expr_idents(*then_expr, env)), - else_expr: Box::new(substitute_expr_idents(*else_expr, env)), - }, - Expr::Call { name, args } => Expr::Call { - name, - args: args - .into_iter() - .map(|arg| substitute_expr_idents(arg, env)) - .collect(), - }, + if let Some(branch) = default_branch { + // As with an else branch, retain the selector guard until all case + // arms are known to assign the same target. + let exhaustive = exhaustive_fallback && parent_condition.is_none(); + let condition = combine_expr_condition_terms(parent_condition, prior_false); + let branch_start = assignments.len(); + conditional_assignments_from_statement_or_null( + branch, + condition.clone(), + false, + retain_unreachable_writes, + syntax_tree, + const_env, + packed_dimensions, + assignments, + )?; + mark_condition_context(assignments, branch_start, chain_start); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); + if exhaustive { + mark_exhaustive_fallback( + &mut assignments[branch_start..], + &definitely_assigned_branches, + chain_start, + packed_dimensions, + ); + } } + Ok(()) } -fn substitute_const_expr_constants( - expr: ConstExpr, - const_env: &HashMap, -) -> ConstExpr { +fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool { match expr { - ConstExpr::Ident(name) => const_env - .get(&name) - .filter(|_| !const_env.contains_key(¶meter_marker(&name))) - .map(|value| ConstExpr::Literal(value.to_string())) - .unwrap_or(ConstExpr::Ident(name)), - ConstExpr::Literal(value) => ConstExpr::Literal(value), - ConstExpr::Select { expr, bit } => ConstExpr::Select { - expr: Box::new(substitute_const_expr_constants(*expr, const_env)), - bit: Box::new(substitute_const_expr_constants(*bit, const_env)), - }, - ConstExpr::Function { name, args } => ConstExpr::Function { - name, - args: args - .into_iter() - .map(|arg| substitute_const_expr_constants(arg, const_env)) - .collect(), - }, - ConstExpr::Unary { op, expr } => ConstExpr::Unary { - op, - expr: Box::new(substitute_const_expr_constants(*expr, const_env)), - }, - ConstExpr::Binary { left, op, right } => ConstExpr::Binary { - left: Box::new(substitute_const_expr_constants(*left, const_env)), - op, - right: Box::new(substitute_const_expr_constants(*right, const_env)), - }, - ConstExpr::Mux { + Expr::Ident(name) => { + packed_dimensions + .get(name) + .is_some_and(|dimensions| dimensions.is_2state) + || packed_dimensions.const_env.contains_key(name) + } + Expr::Literal(value) => typecheck::parse_integral_literal(value) + .is_some_and(|literal| literal.mask == num_bigint::BigUint::default()), + Expr::Select { expr, msb, lsb, .. } => { + expr_is_two_state(expr, packed_dimensions) + && select_bounds_are_statically_valid(expr, msb, lsb, packed_dimensions) + } + Expr::Resize { expr, .. } => expr_is_two_state(expr, packed_dimensions), + Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts + .iter() + .all(|part| expr_is_two_state(part, packed_dimensions)), + Expr::Unary { op, expr } => { + *op == UnaryOp::ToTwoState || expr_is_two_state(expr, packed_dimensions) + } + Expr::Binary { left, op, right } => { + matches!(op, BinaryOp::EqCase | BinaryOp::NeCase) + || (expr_is_two_state(left, packed_dimensions) + && expr_is_two_state(right, packed_dimensions) + && (!matches!(op, BinaryOp::Div | BinaryOp::Mod) + || expr_to_const((**right).clone()) + .and_then(|right| { + eval_ast_const_expr(&right, &packed_dimensions.const_env) + }) + .is_some_and(|right| right != 0))) + } + Expr::Mux { condition, then_expr, else_expr, - } => ConstExpr::Mux { - condition: Box::new(substitute_const_expr_constants(*condition, const_env)), - then_expr: Box::new(substitute_const_expr_constants(*then_expr, const_env)), - else_expr: Box::new(substitute_const_expr_constants(*else_expr, const_env)), - }, + } => { + expr_is_two_state(then_expr, packed_dimensions) + && (then_expr == else_expr + || (expr_is_two_state(condition, packed_dimensions) + && expr_is_two_state(else_expr, packed_dimensions))) + } + Expr::Call { name, .. } => packed_dimensions + .function_return_types + .get(name) + .is_some_and(|metadata| metadata.is_2state), } } -fn assignments_from_continuous_assign( - assign: &sv_parser::ContinuousAssign, - syntax_tree: &SyntaxTree, +fn select_bounds_are_statically_valid( + expr: &Expr, + msb: &ConstExpr, + lsb: &ConstExpr, packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - match assign { - sv_parser::ContinuousAssign::Net(assign) => assign - .nodes - .3 - .nodes - .0 - .contents() - .into_iter() - .map(|assignment| { - let lhs = net_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported("continuous assignment lvalue".to_string()) - })?; - let rhs = expr_from_expression_with_types( - &assignment.nodes.2, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("continuous assignment expression".to_string()) - })?; - Ok(Assignment::new(lhs, rhs)) - }) - .collect(), - sv_parser::ContinuousAssign::Variable(assign) => assign - .nodes - .2 - .nodes - .0 - .contents() - .into_iter() - .map(|assignment| { - let lhs = - variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported("continuous assignment lvalue".to_string()) - })?; - let rhs = expr_from_expression_with_types( - &assignment.nodes.2, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("continuous assignment expression".to_string()) - })?; - Ok(Assignment::new(lhs, rhs)) - }) - .collect(), - } +) -> bool { + let (Some(msb), Some(lsb)) = ( + eval_ast_const_expr(msb, &packed_dimensions.const_env), + eval_ast_const_expr(lsb, &packed_dimensions.const_env), + ) else { + return false; + }; + let (valid_low, valid_high) = if let Expr::Ident(name) = expr { + let Some(LValue::Select { msb, lsb, .. }) = whole_packed_lvalue(name, packed_dimensions) + else { + return false; + }; + let (Some(msb), Some(lsb)) = ( + eval_ast_const_expr(&msb, &packed_dimensions.const_env), + eval_ast_const_expr(&lsb, &packed_dimensions.const_env), + ) else { + return false; + }; + (msb.min(lsb), msb.max(lsb)) + } else { + let Some(width) = expr_static_width(expr, packed_dimensions) + .and_then(|width| width.checked_sub(1)) + .and_then(|high| i128::try_from(high).ok()) + else { + return false; + }; + (0, width) + }; + msb.min(lsb) >= valid_low && msb.max(lsb) <= valid_high } -fn comb_process_from_always_construct( - always: &sv_parser::AlwaysConstruct, - condition: Option, +fn two_state_case_item_reachability( + stmt: &sv_parser::CaseStatementNormal, syntax_tree: &SyntaxTree, + const_env: &HashMap, packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) { - return Ok(None); +) -> Option<(Vec, bool)> { + if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { + return None; } - validate_always_comb_statement(&always.nodes.1)?; - let assignments = assignments_from_statement(&always.nodes.1, syntax_tree, packed_dimensions); - let assignment_count = RefNode::Statement(&always.nodes.1) - .into_iter() - .filter(|node| matches!(node, RefNode::BlockingAssignment(_))) - .count(); - if assignments.len() != assignment_count { - return Err(AnalyzerError::Unsupported( - "always_comb assignment expression".to_string(), + let selector = expr_from_expression_with_types( + &stmt.nodes.2.nodes.1.nodes.0, + syntax_tree, + packed_dimensions, + )?; + let selector = expand_expr_calls( + selector, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + true, + ); + let selector = simplify_constant_mux_conditions(selector, const_env); + let selector = fold_const_integral_expr_preserving_mask(selector, const_env); + let mut labels_by_item = Vec::new(); + let mut default_index = None; + for item in std::iter::once(&stmt.nodes.3).chain(stmt.nodes.4.iter()) { + match item { + sv_parser::CaseItem::NonDefault(item) => { + let labels = item + .nodes + .0 + .contents() + .into_iter() + .map(|label| { + expr_from_expression_with_types( + &label.nodes.0, + syntax_tree, + packed_dimensions, + ) + .map(|label| { + expand_expr_calls( + label, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + true, + ) + }) + .map(|label| simplify_constant_mux_conditions(label, const_env)) + .map(|label| fold_const_integral_expr_preserving_mask(label, const_env)) + .and_then(expr_to_const) + }) + .collect::>>()?; + labels_by_item.push(Some(labels)); + } + sv_parser::CaseItem::Default(_) => { + default_index = Some(labels_by_item.len()); + labels_by_item.push(None); + } + } + } + let (mut duplicate_reachability, mut has_duplicate) = + case_item_duplicate_reachability(&labels_by_item, const_env, None); + let constant_selector = expr_to_const(selector.clone()); + if let Some(constant_selector) = constant_selector.as_ref() + && eval_ast_const_expr(constant_selector, const_env).is_none() + && let Some(reachability) = constant_case_item_reachability( + constant_selector, + &labels_by_item, + default_index, + const_env, + ) + { + return Some(reachability); + } + let Some(width) = expr_static_width(&selector, packed_dimensions) else { + return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); + }; + let mut identifiers = packed_dimensions + .iter() + .map(|(name, dimensions)| (name.clone(), dimensions.signed)) + .collect::>(); + identifiers.extend( + parameter_types_from_const_env(const_env) + .into_iter() + .map(|(name, r#type)| (name, r#type.signed)), + ); + let selector_signed = if let Expr::Call { name, .. } = &selector { + packed_dimensions + .function_return_types + .get(name) + .map(|metadata| metadata.signed) + } else { + expr_signedness(&selector, &identifiers, &HashMap::default()) + }; + let Some(selector_signed) = selector_signed else { + return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); + }; + (duplicate_reachability, has_duplicate) = case_item_duplicate_reachability( + &labels_by_item, + const_env, + Some(ExprType { + width, + signed: selector_signed, + }), + ); + if !expr_is_two_state(&selector, packed_dimensions) { + if let Some(reachability) = finite_four_state_case_item_reachability( + &labels_by_item, + default_index, + width, + selector_signed, + const_env, + ) { + return Some(reachability); + } + return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); + } + // Constant evaluation and the type model use i128 values. Wider dynamic + // selectors still lower normally, but duplicate constant items can still + // be excluded from definite-assignment analysis. + if width > 128 { + return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); + } + let constant_selector_value = constant_selector + .as_ref() + .and_then(|selector| eval_ast_const_expr(selector, const_env)); + let mut reachable = vec![false; labels_by_item.len()]; + if let Some(value) = constant_selector_value { + let selector = ConstExpr::Literal(format_typed_parameter_literal( + value, + width, + selector_signed, )); + let mut matched = None; + for (index, labels) in labels_by_item.iter().enumerate() { + let Some(labels) = labels else { + continue; + }; + for label in labels { + let equal = eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new(selector.clone()), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, + const_env, + )?; + if equal != 0 { + matched = Some(index); + break; + } + } + if matched.is_some() { + break; + } + } + let covered = if let Some(index) = matched { + reachable[index] = true; + true + } else if let Some(index) = default_index { + reachable[index] = true; + true + } else { + false + }; + return Some((reachable, covered)); } - Ok((!assignments.is_empty()) - .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) -} -fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), AnalyzerError> { - match &stmt.nodes.2 { - sv_parser::StatementItem::BlockingAssignment(_) => Ok(()), - sv_parser::StatementItem::SeqBlock(block) => { - for stmt in &block.nodes.3 { - if let sv_parser::StatementOrNull::Statement(stmt) = stmt { - validate_always_comb_statement(stmt)?; - } + // A constant case label can match at most one bit pattern of a two-state + // selector. Track those patterns directly instead of materializing every + // value in the selector's 2^width domain. + let mut matched_values = HashSet::default(); + for (index, labels) in labels_by_item.iter().enumerate() { + let Some(labels) = labels else { + continue; + }; + for label in labels { + let Some(label_value) = eval_ast_const_expr(label, const_env) else { + // X/Z-bearing labels cannot match a two-state selector. + continue; + }; + let pattern = if width == 128 { + label_value as u128 + } else { + let mask = 1u128.checked_shl(u32::try_from(width).ok()?)? - 1; + label_value as u128 & mask + }; + let candidate = ConstExpr::Literal(format_typed_parameter_literal( + pattern as i128, + width, + selector_signed, + )); + let equal = eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new(candidate), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, + const_env, + )?; + if equal != 0 && matched_values.insert(pattern) { + reachable[index] = true; } - Ok(()) } - _ => Err(AnalyzerError::Unsupported( - "unsupported statement inside always_comb".to_string(), - )), + } + let domain_is_covered = u32::try_from(width) + .ok() + .and_then(|width| 1usize.checked_shl(width)) + .is_some_and(|value_count| matched_values.len() == value_count); + if let Some(index) = default_index { + reachable[index] = !domain_is_covered; + Some((reachable, true)) + } else { + Some((reachable, domain_is_covered)) } } -fn ff_processes_from_module_node( - node: RefNode<'_>, - syntax_tree: &SyntaxTree, +fn constant_case_item_reachability( + selector: &ConstExpr, + labels_by_item: &[Option>], + default_index: Option, const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - let mut processes = Vec::new(); - for item in module_non_port_items(node) { - ff_processes_from_non_port_module_item( - item, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - &mut processes, - )?; +) -> Option<(Vec, bool)> { + let mut reachable = vec![false; labels_by_item.len()]; + let mut matched = None; + for (index, labels) in labels_by_item.iter().enumerate() { + let Some(labels) = labels else { + continue; + }; + for label in labels { + let equal = eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new(selector.clone()), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, + const_env, + )?; + if equal != 0 { + matched = Some(index); + break; + } + } + if matched.is_some() { + break; + } } - Ok(processes) + let covered = if let Some(index) = matched.or(default_index) { + reachable[index] = true; + true + } else { + false + }; + Some((reachable, covered)) } -fn ff_processes_from_non_port_module_item( - item: &sv_parser::NonPortModuleItem, - syntax_tree: &SyntaxTree, +fn finite_four_state_case_item_reachability( + labels_by_item: &[Option>], + default_index: Option, + width: usize, + selector_signed: bool, const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::NonPortModuleItem::GenerateRegion(region) => { - for item in ®ion.nodes.1 { - ff_processes_from_generate_item( - item, - syntax_tree, +) -> Option<(Vec, bool)> { + const MAX_PATTERNS: usize = 65_536; + let state_bits = width.checked_mul(2)?; + let pattern_count = 1usize.checked_shl(u32::try_from(state_bits).ok()?)?; + if pattern_count > MAX_PATTERNS { + return None; + } + let mut reachable = vec![false; labels_by_item.len()]; + let mut covered = true; + for pattern in 0..pattern_count { + let bits = (0..width) + .rev() + .map(|bit| match (pattern >> (bit * 2)) & 3 { + 0 => '0', + 1 => '1', + 2 => 'x', + 3 => 'z', + _ => unreachable!(), + }) + .collect::(); + let signing = if selector_signed { "s" } else { "" }; + let selector = ConstExpr::Literal(format!("{width}'{signing}b{bits}")); + let mut matched = None; + for (index, labels) in labels_by_item.iter().enumerate() { + let Some(labels) = labels else { + continue; + }; + for label in labels { + let equal = eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new(selector.clone()), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, const_env, - parameter_literals, - packed_dimensions, - processes, )?; + if equal != 0 { + matched = Some(index); + break; + } + } + if matched.is_some() { + break; } } - sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { - ff_processes_from_module_or_generate_item( - item, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - processes, - )?; + if let Some(index) = matched.or(default_index) { + reachable[index] = true; + } else { + covered = false; } - _ => {} - } - Ok(()) -} - -fn ff_processes_from_generate_item( - item: &sv_parser::GenerateItem, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { - ff_processes_from_module_or_generate_item( - item, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - processes, - )?; } - Ok(()) + Some((reachable, covered)) } -fn ff_processes_from_module_or_generate_item( - item: &sv_parser::ModuleOrGenerateItem, - syntax_tree: &SyntaxTree, +fn case_item_duplicate_reachability( + labels_by_item: &[Option>], const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { - ff_processes_from_module_common_item( - &item.nodes.1, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - processes, - )?; + selector_type: Option, +) -> (Vec, bool) { + let constant_types = parameter_types_from_const_env(const_env) + .into_iter() + .map(|(name, r#type)| (name, (r#type.width, r#type.signed))) + .collect::>(); + let mut reachable = vec![false; labels_by_item.len()]; + let mut prior_labels: Vec<&ConstExpr> = Vec::new(); + let mut has_duplicate = false; + for (index, labels) in labels_by_item.iter().enumerate() { + let Some(labels) = labels else { + reachable[index] = true; + continue; + }; + for label in labels { + let duplicate = prior_labels.iter().any(|prior| { + let normalized_equal = selector_type.is_some_and(|selector_type| { + let prior = case_label_selector_pattern( + prior, + const_env, + &constant_types, + selector_type, + ); + let label = case_label_selector_pattern( + label, + const_env, + &constant_types, + selector_type, + ); + prior.zip(label).is_some_and(|(prior, label)| { + prior.value == label.value && prior.mask == label.mask + }) + }); + normalized_equal + || *prior == label + || eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new((*prior).clone()), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, + const_env, + ) == Some(1) + }); + has_duplicate |= duplicate; + reachable[index] |= !duplicate; + prior_labels.push(label); + } } - Ok(()) + (reachable, has_duplicate) } -fn ff_processes_from_module_common_item( - item: &sv_parser::ModuleCommonItem, - syntax_tree: &SyntaxTree, +fn case_label_selector_pattern( + label: &ConstExpr, const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - match item { - sv_parser::ModuleCommonItem::AlwaysConstruct(always) => { - if let Some(process) = ff_process_from_always_construct( - always, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - )? { - processes.push(process); - } - } - sv_parser::ModuleCommonItem::ConditionalGenerateConstruct(generate) => { - let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { - return Ok(()); - }; - let Some(condition) = const_expr_from_ref_node( - RefNode::ConstantExpression(&generate.nodes.1.nodes.1), - syntax_tree, - ) else { - return Err(AnalyzerError::Unsupported( - "unknown conditional-generate condition".to_string(), - )); - }; - let Some(condition_value) = eval_ast_const_expr(&condition, const_env) else { - return Err(AnalyzerError::Unsupported( - "unknown conditional-generate condition".to_string(), - )); - }; - let block = if condition_value != 0 { - Some(&generate.nodes.2) - } else { - generate.nodes.3.as_ref().map(|(_, block)| block) - }; - if let Some(block) = block { - ff_processes_from_generate_block( - block, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - processes, - )?; + constant_types: &HashMap, + selector_type: ExprType, +) -> Option { + let label_expr: crate::ir::ConstExpr = label.clone().into(); + if let ConstExpr::Literal(value) = label + && let Some(resized) = + resize_unbased_fill_literal_for_cast(value, selector_type.width, selector_type.signed) + { + return typecheck::parse_integral_literal(&resized); + } + let mut literal = + typecheck::eval_const_integral_literal_with_types(&label_expr, const_env, constant_types)?; + let comparison_signed = selector_type.signed && literal.signed; + if literal.width > selector_type.width { + let extension_bit = u64::try_from(selector_type.width.checked_sub(1)?).ok()?; + let extension_value = comparison_signed && literal.value.bit(extension_bit); + let extension_mask = comparison_signed && literal.mask.bit(extension_bit); + for bit in selector_type.width..literal.width { + let bit = bit as u64; + if literal.value.bit(bit) != extension_value || literal.mask.bit(bit) != extension_mask + { + return None; } } - _ => {} } - Ok(()) + literal.signed = comparison_signed; + let resized = + resize_integral_literal_for_cast(literal, selector_type.width, selector_type.signed); + typecheck::parse_integral_literal(&resized) } - -fn ff_processes_from_generate_block( - block: &sv_parser::GenerateBlock, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - parameter_literals: &HashMap, - packed_dimensions: &PackedDimensions, - processes: &mut Vec, -) -> Result<(), AnalyzerError> { - match block { - sv_parser::GenerateBlock::GenerateItem(item) => { - ff_processes_from_generate_item( - item, - syntax_tree, - const_env, - parameter_literals, - packed_dimensions, - processes, - )?; - } - sv_parser::GenerateBlock::Multiple(block) => { - let mut block_env = const_env.clone(); - let mut block_parameter_literals = parameter_literals.clone(); - for item in &block.nodes.3 { - if add_localparams_from_generate_item_with_literals( - item, - syntax_tree, - &mut block_env, - Some(&mut block_parameter_literals), - ) { - continue; - } - ff_processes_from_generate_item( - item, - syntax_tree, - &block_env, - &block_parameter_literals, - packed_dimensions, - processes, - )?; - } + +fn mark_condition_context( + assignments: &mut [ConditionalAssignment], + start: usize, + guard_boundary: usize, +) { + let epoch = assignments + .iter() + .flat_map(|assignment| assignment.path_epochs.iter().copied()) + .max() + .map_or(0, |epoch| epoch + 1); + for assignment in &mut assignments[start..] { + if assignment.condition.is_some() { + assignment.guard_boundary.get_or_insert(guard_boundary); + assignment.path_epochs.push(epoch); } } - Ok(()) } -fn ff_process_from_always_construct( - always: &sv_parser::AlwaysConstruct, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - parameter_literals: &HashMap, +fn mark_exhaustive_fallback( + fallback_assignments: &mut [ConditionalAssignment], + branch_targets: &[Vec], + chain_start: usize, packed_dimensions: &PackedDimensions, -) -> Result, AnalyzerError> { - if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysFf(_)) { - return Ok(None); +) { + let mut definite_targets = Vec::new(); + for assignment in fallback_assignments.iter() { + let target = assignment.assignment().lhs_value().clone(); + if branch_targets + .iter() + .all(|targets| lvalue_is_covered_by(&target, targets, packed_dimensions)) + && !definite_targets.contains(&target) + { + definite_targets.push(target); + } + } + for target in definite_targets { + let candidates = fallback_assignments + .iter() + .enumerate() + .filter(|(_, assignment)| assignment.assignment().lhs_value() == &target) + .collect::>(); + let marked = candidates + .iter() + .find(|(_, assignment)| assignment.exhaustive_fallback_start.is_some()) + .map(|(index, _)| *index); + let shallowest = candidates + .iter() + .min_by_key(|(index, assignment)| (assignment.path_epochs.len(), *index)) + .map(|(index, _)| *index); + if let Some(index) = marked.or(shallowest) { + fallback_assignments[index].exhaustive_fallback_start = Some(chain_start); + } } - let Some((events, body)) = ff_event_control_and_body(&always.nodes.1, syntax_tree) else { - return Err(AnalyzerError::Unsupported( - "always_ff event expression".to_string(), - )); - }; - let mut assignments = Vec::new(); - conditional_assignments_from_statement_or_null( - body, - None, - syntax_tree, - const_env, - packed_dimensions, - &mut assignments, - )?; - let assignments = assignments - .into_iter() - .map(|assignment| { - ConditionalAssignment::new( - assignment.condition.map(|condition| { - substitute_expr_constants_with_parameter_literals( - condition, - const_env, - parameter_literals, - ) - }), - substitute_assignment_constants_with_parameter_literals( - assignment.assignment, - const_env, - parameter_literals, - ), - ) - }) - .collect::>(); - Ok( - (!events.is_empty() && !assignments.is_empty()) - .then(|| FfProcess::new(events, assignments)), - ) } -fn ff_event_control_and_body<'a>( - stmt: &'a sv_parser::Statement, +fn definitely_assigned_comb_targets_statement_or_null( + stmt: &sv_parser::StatementOrNull, syntax_tree: &SyntaxTree, -) -> Option<(Vec, &'a sv_parser::StatementOrNull)> { - let sv_parser::StatementItem::ProceduralTimingControlStatement(timing) = &stmt.nodes.2 else { - return None; - }; - let sv_parser::ProceduralTimingControl::EventControl(event_control) = &timing.nodes.0 else { - return None; - }; - Some(( - ff_events_from_event_control(event_control, syntax_tree)?, - &timing.nodes.1, - )) + packed_dimensions: &PackedDimensions, +) -> Vec { + match stmt { + sv_parser::StatementOrNull::Statement(stmt) => { + definitely_assigned_comb_targets(stmt, syntax_tree, packed_dimensions) + } + sv_parser::StatementOrNull::Attribute(_) => Vec::new(), + } } -fn ff_events_from_event_control( - control: &sv_parser::EventControl, +fn written_comb_targets_statement_or_null( + stmt: &sv_parser::StatementOrNull, syntax_tree: &SyntaxTree, -) -> Option> { - let sv_parser::EventControl::EventExpression(control) = control else { - return None; + packed_dimensions: &PackedDimensions, +) -> Option> { + let sv_parser::StatementOrNull::Statement(stmt) = stmt else { + return Some(Vec::new()); }; - ff_events_from_event_expression(&control.nodes.1.nodes.1, syntax_tree) + written_comb_targets(stmt, syntax_tree, packed_dimensions) } -fn ff_events_from_event_expression( - expr: &sv_parser::EventExpression, +fn written_comb_targets( + stmt: &sv_parser::Statement, syntax_tree: &SyntaxTree, -) -> Option> { - match expr { - sv_parser::EventExpression::Expression(expr) => { - let edge = match expr.nodes.0.as_ref()? { - sv_parser::EdgeIdentifier::Posedge(_) => FfEdge::Pos, - sv_parser::EdgeIdentifier::Negedge(_) => FfEdge::Neg, - sv_parser::EdgeIdentifier::Edge(_) => return None, - }; - let signal = expr_ident_name(&expr_from_expression(&expr.nodes.1, syntax_tree)?); - signal.map(|signal| vec![FfEvent::new(edge, signal)]) + packed_dimensions: &PackedDimensions, +) -> Option> { + let collect = |nested: &sv_parser::StatementOrNull, targets: &mut Vec| { + for target in + written_comb_targets_statement_or_null(nested, syntax_tree, packed_dimensions)? + { + if !targets.contains(&target) { + targets.push(target); + } } - sv_parser::EventExpression::Or(expr) => { - let mut left = ff_events_from_event_expression(&expr.nodes.0, syntax_tree)?; - left.extend(ff_events_from_event_expression(&expr.nodes.2, syntax_tree)?); - Some(left) + Some(()) + }; + match &stmt.nodes.2 { + sv_parser::StatementItem::BlockingAssignment(assignment) => { + let target = match &assignment.0 { + sv_parser::BlockingAssignment::Variable(assignment) => { + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + } + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + } + _ => None, + }?; + Some(vec![target]) } - sv_parser::EventExpression::Comma(expr) => { - let mut left = ff_events_from_event_expression(&expr.nodes.0, syntax_tree)?; - left.extend(ff_events_from_event_expression(&expr.nodes.2, syntax_tree)?); - Some(left) + sv_parser::StatementItem::NonblockingAssignment(assignment) => { + Some(vec![variable_lvalue_from_node( + &assignment.0.nodes.0, + syntax_tree, + packed_dimensions, + )?]) } - sv_parser::EventExpression::Paren(expr) => { - ff_events_from_event_expression(&expr.nodes.0.nodes.1, syntax_tree) + sv_parser::StatementItem::SeqBlock(block) => { + let mut targets = Vec::new(); + for stmt in &block.nodes.3 { + collect(stmt, &mut targets)?; + } + Some(targets) + } + sv_parser::StatementItem::ConditionalStatement(conditional) => { + let mut targets = Vec::new(); + collect(&conditional.nodes.3, &mut targets)?; + for (_, _, _, branch) in &conditional.nodes.4 { + collect(branch, &mut targets)?; + } + if let Some((_, branch)) = &conditional.nodes.5 { + collect(branch, &mut targets)?; + } + Some(targets) + } + sv_parser::StatementItem::CaseStatement(case) => { + let sv_parser::CaseStatement::Normal(case) = &**case else { + return None; + }; + let mut targets = Vec::new(); + for item in std::iter::once(&case.nodes.3).chain(case.nodes.4.iter()) { + let branch = match item { + sv_parser::CaseItem::NonDefault(item) => &item.nodes.2, + sv_parser::CaseItem::Default(item) => &item.nodes.2, + }; + collect(branch, &mut targets)?; + } + Some(targets) + } + sv_parser::StatementItem::LoopStatement(loop_statement) => { + let (name, values) = static_for_loop_iterations( + loop_statement, + syntax_tree, + &packed_dimensions.const_env, + )?; + if values.is_empty() { + return Some(Vec::new()); + } + let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { + return None; + }; + let mut targets = Vec::new(); + for value in values { + let mut iteration_dimensions = packed_dimensions.clone(); + iteration_dimensions.const_env.insert(name.clone(), value); + insert_parameter_type_markers( + &mut iteration_dimensions.const_env, + &name, + ExprType { + width: 32, + signed: true, + }, + ); + for target in written_comb_targets_statement_or_null( + &loop_statement.nodes.2, + syntax_tree, + &iteration_dimensions, + )? { + let target = + substitute_lvalue_constants(target, &iteration_dimensions.const_env); + if !targets.contains(&target) { + targets.push(target); + } + } + } + Some(targets) } _ => None, } } -fn conditional_assignments_from_statement_or_null( - stmt: &sv_parser::StatementOrNull, - condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, - assignments: &mut Vec, -) -> Result<(), AnalyzerError> { - if let sv_parser::StatementOrNull::Statement(stmt) = stmt { - conditional_assignments_from_statement( - stmt, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; - } - Ok(()) -} - -fn conditional_assignments_from_statement( +fn definitely_assigned_comb_targets( stmt: &sv_parser::Statement, - condition: Option, syntax_tree: &SyntaxTree, - const_env: &HashMap, packed_dimensions: &PackedDimensions, - assignments: &mut Vec, -) -> Result<(), AnalyzerError> { +) -> Vec { match &stmt.nodes.2 { - sv_parser::StatementItem::NonblockingAssignment(assignment) => { - let lhs = - variable_lvalue_from_node(&assignment.0.nodes.0, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported("always_ff assignment lowering".to_string()) - })?; - let rhs = expr_from_expression_with_types( - &assignment.0.nodes.3, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported("always_ff assignment lowering".to_string()) - })?; - assignments.push(ConditionalAssignment::new( - condition, - Assignment::new(lhs, rhs), - )); + sv_parser::StatementItem::BlockingAssignment(assignment) => { + let target = match &assignment.0 { + sv_parser::BlockingAssignment::Variable(assignment) => { + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + } + sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { + variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions) + } + _ => None, + }; + target.into_iter().collect() } sv_parser::StatementItem::SeqBlock(block) => { + let mut targets = Vec::new(); + let mut guarded_targets: Vec<(Expr, Vec)> = Vec::new(); for stmt in &block.nodes.3 { - conditional_assignments_from_statement_or_null( + let written_targets = + written_comb_targets_statement_or_null(stmt, syntax_tree, packed_dimensions); + let statement_targets = definitely_assigned_comb_targets_statement_or_null( stmt, - condition.clone(), syntax_tree, - const_env, packed_dimensions, - assignments, - )?; + ); + if let Some(written_targets) = &written_targets { + guarded_targets.retain(|(condition, _)| { + !written_targets.iter().any(|target| { + expr_references_overlapping_lvalue( + condition, + target, + &packed_dimensions.const_env, + ) + }) + }); + } else { + guarded_targets.clear(); + } + for target in statement_targets { + if !targets.contains(&target) { + targets.push(target); + } + } + let Some((condition, branch_targets)) = + guarded_comb_targets(stmt, syntax_tree, packed_dimensions) + else { + continue; + }; + guarded_targets.retain(|(prior_condition, _)| { + !branch_targets.iter().any(|target| { + expr_references_overlapping_lvalue( + prior_condition, + target, + &packed_dimensions.const_env, + ) + }) + }); + for (prior_condition, prior_targets) in &guarded_targets { + if !two_state_conditions_are_complements( + prior_condition, + &condition, + packed_dimensions, + ) { + continue; + } + for target in intersect_lvalue_sets( + vec![prior_targets.clone(), branch_targets.clone()], + packed_dimensions, + ) { + if !targets.contains(&target) { + targets.push(target); + } + } + } + guarded_targets.push((condition, branch_targets)); + } + targets + } + sv_parser::StatementItem::ConditionalStatement(conditional) => { + let mut branches = Vec::new(); + let mut terminal = false; + for (predicate, branch) in + std::iter::once((&conditional.nodes.2.nodes.1, &conditional.nodes.3)).chain( + conditional + .nodes + .4 + .iter() + .map(|(_, _, predicate, branch)| (&predicate.nodes.1, branch)), + ) + { + let condition = expr_from_cond_predicate(predicate, syntax_tree, packed_dimensions) + .map(|condition| { + expand_expr_calls( + condition, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + true, + ) + }) + .map(|condition| { + simplify_constant_mux_conditions(condition, &packed_dimensions.const_env) + }) + .map(procedural_truth_condition) + .and_then(expr_to_const) + .and_then(|condition| { + eval_ast_const_expr(&condition, &packed_dimensions.const_env) + }); + if condition == Some(0) { + continue; + } + branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); + if condition.is_some() { + terminal = true; + break; + } } + if !terminal { + let Some((_, else_branch)) = &conditional.nodes.5 else { + return Vec::new(); + }; + branches.push(definitely_assigned_comb_targets_statement_or_null( + else_branch, + syntax_tree, + packed_dimensions, + )); + } + intersect_lvalue_sets(branches, packed_dimensions) } - sv_parser::StatementItem::ConditionalStatement(stmt) => { - conditional_assignments_from_conditional_statement( - stmt, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; - } - sv_parser::StatementItem::CaseStatement(stmt) => { - conditional_assignments_from_case_statement( - stmt, - condition, + sv_parser::StatementItem::CaseStatement(case) => { + let sv_parser::CaseStatement::Normal(case) = &**case else { + return Vec::new(); + }; + let reachability = two_state_case_item_reachability( + case, syntax_tree, - const_env, + &packed_dimensions.const_env, packed_dimensions, - assignments, - )?; + ); + let complete_two_state_case = + reachability.as_ref().is_some_and(|(_, covered)| *covered); + let mut has_default = false; + let branches = std::iter::once(&case.nodes.3) + .chain(case.nodes.4.iter()) + .enumerate() + .filter_map(|(index, item)| { + if reachability + .as_ref() + .is_some_and(|(reachable, _)| !reachable[index]) + { + return None; + } + let branch = match item { + sv_parser::CaseItem::NonDefault(item) => &item.nodes.2, + sv_parser::CaseItem::Default(item) => { + has_default = true; + &item.nodes.2 + } + }; + Some(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )) + }) + .collect::>(); + if has_default || complete_two_state_case { + intersect_lvalue_sets(branches, packed_dimensions) + } else { + Vec::new() + } } sv_parser::StatementItem::LoopStatement(loop_statement) => { - let (name, values) = static_for_loop_iterations(loop_statement, syntax_tree, const_env) - .ok_or_else(|| { - AnalyzerError::Unsupported("unsupported procedural for loop".to_string()) - })?; - let body = match &**loop_statement { - sv_parser::LoopStatement::For(loop_statement) => &loop_statement.nodes.2, - _ => unreachable!(), + let Some((name, values)) = static_for_loop_iterations( + loop_statement, + syntax_tree, + &packed_dimensions.const_env, + ) else { + return Vec::new(); + }; + let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { + return Vec::new(); }; + let mut targets = Vec::new(); for value in values { - let mut loop_env = const_env.clone(); - loop_env.insert(name.clone(), value); - let mut loop_packed_dimensions = packed_dimensions.clone(); - let mut loop_const_env = loop_env.clone(); + let mut iteration_dimensions = packed_dimensions.clone(); + iteration_dimensions.const_env.insert(name.clone(), value); insert_parameter_type_markers( - &mut loop_const_env, + &mut iteration_dimensions.const_env, &name, ExprType { width: 32, signed: true, }, ); - loop_packed_dimensions.const_env = loop_const_env.clone(); - let start = assignments.len(); - conditional_assignments_from_statement_or_null( - body, - condition.clone(), + for target in definitely_assigned_comb_targets_statement_or_null( + &loop_statement.nodes.2, syntax_tree, - &loop_const_env, - &loop_packed_dimensions, - assignments, - )?; - for assignment in &mut assignments[start..] { - assignment.condition = assignment.condition.take().map(|condition| { - substitute_expr_constants_with_parameter_literals( - condition, - &loop_env, - &HashMap::default(), - ) - }); - assignment.assignment = - substitute_assignment_constants(assignment.assignment.clone(), &loop_env); + &iteration_dimensions, + ) { + let target = + substitute_lvalue_constants(target, &iteration_dimensions.const_env); + if !targets.contains(&target) { + targets.push(target); + } } } + targets } - _ => { - return Err(AnalyzerError::Unsupported( - "unsupported statement inside always_ff".to_string(), - )); - } + _ => Vec::new(), } - Ok(()) } -fn conditional_assignments_from_conditional_statement( - stmt: &sv_parser::ConditionalStatement, - parent_condition: Option, +fn guarded_comb_targets( + stmt: &sv_parser::StatementOrNull, syntax_tree: &SyntaxTree, - const_env: &HashMap, packed_dimensions: &PackedDimensions, - assignments: &mut Vec, -) -> Result<(), AnalyzerError> { - let if_condition = - expr_from_cond_predicate(&stmt.nodes.2.nodes.1, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) - })?; - let mut prior_false = Vec::new(); - let then_condition = combine_expr_conditions(parent_condition.clone(), if_condition.clone()); - conditional_assignments_from_statement_or_null( - &stmt.nodes.3, - then_condition, +) -> Option<(Expr, Vec)> { + let sv_parser::StatementOrNull::Statement(stmt) = stmt else { + return None; + }; + let sv_parser::StatementItem::ConditionalStatement(conditional) = &stmt.nodes.2 else { + return None; + }; + if !conditional.nodes.4.is_empty() || conditional.nodes.5.is_some() { + return None; + } + let condition = + expr_from_cond_predicate(&conditional.nodes.2.nodes.1, syntax_tree, packed_dimensions)?; + let condition = expand_expr_calls( + condition, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + true, + ); + let targets = definitely_assigned_comb_targets_statement_or_null( + &conditional.nodes.3, syntax_tree, - const_env, packed_dimensions, - assignments, - )?; - prior_false.push(procedural_false_condition(if_condition)); - - for (_, _, predicate, branch) in &stmt.nodes.4 { - let branch_condition = - expr_from_cond_predicate(&predicate.nodes.1, syntax_tree, packed_dimensions) - .ok_or_else(|| { - AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) - })?; - let mut terms = prior_false.clone(); - terms.push(branch_condition.clone()); - let condition = combine_expr_condition_terms(parent_condition.clone(), terms); - conditional_assignments_from_statement_or_null( - branch, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; - prior_false.push(procedural_false_condition(branch_condition)); - } - - if let Some((_, branch)) = &stmt.nodes.5 { - let condition = combine_expr_condition_terms(parent_condition, prior_false); - conditional_assignments_from_statement_or_null( - branch, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; + ); + if targets.is_empty() + || targets.iter().any(|target| { + expr_references_overlapping_lvalue(&condition, target, &packed_dimensions.const_env) + }) + { + return None; } - Ok(()) + Some((condition, targets)) } -fn procedural_false_condition(condition: Expr) -> Expr { - Expr::Unary { - op: UnaryOp::LogicNot, - expr: Box::new(Expr::Unary { - op: UnaryOp::ToTwoState, - expr: Box::new(condition), - }), +fn two_state_conditions_are_complements( + left: &Expr, + right: &Expr, + packed_dimensions: &PackedDimensions, +) -> bool { + if let (Some((left, left_positive)), Some((right, right_positive))) = ( + normalized_two_state_boolean(left, packed_dimensions), + normalized_two_state_boolean(right, packed_dimensions), + ) && left == right + && left_positive != right_positive + { + return true; } + let is_complement = |candidate: &Expr, other: &Expr| { + matches!( + candidate, + Expr::Unary { + op: UnaryOp::LogicNot, + expr, + } if &**expr == other && expr_is_two_state(other, packed_dimensions) + ) + }; + let are_inverse_equalities = |left: &Expr, right: &Expr| { + let ( + Expr::Binary { + left: left_lhs, + op: left_op, + right: left_rhs, + }, + Expr::Binary { + left: right_lhs, + op: right_op, + right: right_rhs, + }, + ) = (left, right) + else { + return false; + }; + let operands_match = (left_lhs == right_lhs && left_rhs == right_rhs) + || (left_lhs == right_rhs && left_rhs == right_lhs); + if !operands_match { + return false; + } + match (left_op, right_op) { + (BinaryOp::Eq, BinaryOp::Ne) | (BinaryOp::Ne, BinaryOp::Eq) => { + expr_is_two_state(left_lhs, packed_dimensions) + && expr_is_two_state(left_rhs, packed_dimensions) + } + (BinaryOp::EqCase, BinaryOp::NeCase) | (BinaryOp::NeCase, BinaryOp::EqCase) => true, + _ => false, + } + }; + is_complement(left, right) || is_complement(right, left) || are_inverse_equalities(left, right) } -fn conditional_assignments_from_case_statement( - stmt: &sv_parser::CaseStatement, - parent_condition: Option, - syntax_tree: &SyntaxTree, - const_env: &HashMap, +fn normalized_two_state_boolean<'a>( + expr: &'a Expr, packed_dimensions: &PackedDimensions, - assignments: &mut Vec, -) -> Result<(), AnalyzerError> { - let sv_parser::CaseStatement::Normal(stmt) = stmt else { - return Ok(()); - }; - if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { - return Ok(()); +) -> Option<(&'a Expr, bool)> { + if let Expr::Unary { op, expr } = expr + && (*op == UnaryOp::LogicNot + || (*op == UnaryOp::BitNot + && expr_static_width(expr, packed_dimensions) == Some(1) + && expr_is_two_state(expr, packed_dimensions))) + { + let (expr, positive) = normalized_two_state_boolean(expr, packed_dimensions)?; + return Some((expr, !positive)); } - let case_expr = expr_from_expression_with_types( - &stmt.nodes.2.nodes.1.nodes.0, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| AnalyzerError::Unsupported("always_ff case selector lowering".to_string()))?; - - let mut branches = Vec::new(); - let mut default_branch = None; - for item in std::iter::once(&stmt.nodes.3).chain(stmt.nodes.4.iter()) { - match item { - sv_parser::CaseItem::NonDefault(item) => { - let mut conditions = Vec::new(); - for expr in item.nodes.0.contents() { - let expr = expr_from_expression_with_types( - &expr.nodes.0, - syntax_tree, - packed_dimensions, - ) - .ok_or_else(|| { - AnalyzerError::Unsupported( - "always_ff case item expression lowering".to_string(), - ) - })?; - conditions.push(case_item_condition(case_expr.clone(), expr)); - } - if let Some(condition) = conditions.into_iter().reduce(|left, right| Expr::Binary { - left: Box::new(left), - op: BinaryOp::LogicOr, - right: Box::new(right), - }) { - branches.push((condition, &item.nodes.2)); - } - } - sv_parser::CaseItem::Default(item) => { - default_branch = Some(&item.nodes.2); - } + if let Expr::Binary { left, op, right } = expr + && matches!( + op, + BinaryOp::Eq | BinaryOp::Ne | BinaryOp::EqCase | BinaryOp::NeCase + ) + { + let nonzero_side = if expr_is_constant_zero(left, &packed_dimensions.const_env) { + &**right + } else if expr_is_constant_zero(right, &packed_dimensions.const_env) { + &**left + } else { + return expr_is_two_state(expr, packed_dimensions).then_some((expr, true)); + }; + if expr_is_two_state(nonzero_side, packed_dimensions) { + return Some((nonzero_side, matches!(op, BinaryOp::Ne | BinaryOp::NeCase))); } } + expr_is_two_state(expr, packed_dimensions).then_some((expr, true)) +} - let mut prior_false = Vec::new(); - for (branch_condition, branch) in branches { - let mut terms = prior_false.clone(); - terms.push(branch_condition.clone()); - let condition = combine_expr_condition_terms(parent_condition.clone(), terms); - conditional_assignments_from_statement_or_null( - branch, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; - prior_false.push(Expr::Unary { - op: UnaryOp::LogicNot, - expr: Box::new(branch_condition), - }); +fn expr_is_constant_zero(expr: &Expr, const_env: &HashMap) -> bool { + expr_to_const(expr.clone()).and_then(|expr| eval_ast_const_expr(&expr, const_env)) == Some(0) +} + +fn intersect_lvalue_sets( + sets: Vec>, + packed_dimensions: &PackedDimensions, +) -> Vec { + if sets.is_empty() { + return Vec::new(); + } + let mut candidates = Vec::new(); + for target in sets.iter().flatten() { + if !candidates.contains(target) { + candidates.push(target.clone()); + } } + candidates.retain(|target| { + sets.iter() + .all(|set| lvalue_is_covered_by(target, set, packed_dimensions)) + }); + candidates +} - if let Some(branch) = default_branch { - let condition = combine_expr_condition_terms(parent_condition, prior_false); - conditional_assignments_from_statement_or_null( - branch, - condition, - syntax_tree, - const_env, - packed_dimensions, - assignments, - )?; +fn lvalue_is_covered_by( + target: &LValue, + writes: &[LValue], + packed_dimensions: &PackedDimensions, +) -> bool { + if writes.contains(target) { + return true; } - Ok(()) + let Some((target_name, target_low, target_high)) = lvalue_bit_range(target, packed_dimensions) + else { + return false; + }; + let mut ranges = writes + .iter() + .filter_map(|write| lvalue_bit_range(write, packed_dimensions)) + .filter_map(|(name, low, high)| (name == target_name).then_some((low, high))) + .collect::>(); + ranges.sort_unstable_by_key(|(low, _)| *low); + let mut covered_through = target_low.checked_sub(1); + for (low, high) in ranges { + let next = covered_through.and_then(|covered| covered.checked_add(1)); + if next.is_some_and(|next| low > next) { + continue; + } + if low <= target_low || next.is_some_and(|next| low <= next) { + covered_through = Some(covered_through.map_or(high, |covered| covered.max(high))); + } + if covered_through.is_some_and(|covered| covered >= target_high) { + return true; + } + } + false +} + +fn lvalue_bit_range( + value: &LValue, + packed_dimensions: &PackedDimensions, +) -> Option<(String, i128, i128)> { + let selected; + let value = match value { + LValue::Ident(name) => { + selected = whole_packed_lvalue(name, packed_dimensions)?; + &selected + } + value => value, + }; + let LValue::Select { name, msb, lsb, .. } = value else { + unreachable!(); + }; + let msb = eval_ast_const_expr(msb, &packed_dimensions.const_env)?; + let lsb = eval_ast_const_expr(lsb, &packed_dimensions.const_env)?; + Some((name.clone(), msb.min(lsb), msb.max(lsb))) } fn expr_from_cond_predicate( @@ -7599,13 +12936,38 @@ fn expr_from_cond_predicate( syntax_tree: &SyntaxTree, packed_dimensions: &PackedDimensions, ) -> Option { - let first = predicate.nodes.0.contents().into_iter().next()?; - let sv_parser::ExpressionOrCondPattern::Expression(expr) = first else { + if cond_predicate_has_conjunction_operator(predicate, syntax_tree) { + return None; + } + let entries = predicate.nodes.0.contents(); + let [sv_parser::ExpressionOrCondPattern::Expression(expr)] = entries.as_slice() else { return None; }; expr_from_expression_with_types(expr, syntax_tree, packed_dimensions) } +fn cond_predicate_has_conjunction_operator( + predicate: &sv_parser::CondPredicate, + syntax_tree: &SyntaxTree, +) -> bool { + let mut previous = None; + for node in RefNode::CondPredicate(predicate) { + let RefNode::Symbol(symbol) = node else { + continue; + }; + let locate = symbol.nodes.0; + if previous.is_some_and(|previous: sv_parser::Locate| { + previous.offset + previous.len == locate.offset + && syntax_tree.get_str(&previous) == Some("&&") + && syntax_tree.get_str(&locate) == Some("&") + }) { + return true; + } + previous = Some(locate); + } + false +} + fn combine_expr_conditions(parent: Option, child: Expr) -> Option { combine_expr_condition_terms(parent, vec![child]) } @@ -7628,72 +12990,6 @@ fn combine_expr_condition_terms(parent: Option, terms: Vec) -> Optio }) } -fn assignments_from_statement( - stmt: &sv_parser::Statement, - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, -) -> Vec { - match &stmt.nodes.2 { - sv_parser::StatementItem::BlockingAssignment(assignment) => match &assignment.0 { - sv_parser::BlockingAssignment::Variable(assignment) => { - let lhs = - variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions); - let rhs = expr_from_expression_with_types( - &assignment.nodes.3, - syntax_tree, - packed_dimensions, - ); - match (lhs, rhs) { - (Some(lhs), Some(rhs)) => vec![Assignment::new(lhs, rhs)], - _ => Vec::new(), - } - } - sv_parser::BlockingAssignment::OperatorAssignment(assignment) => { - let op = syntax_tree.get_str(&assignment.nodes.1.nodes.0.nodes.0); - let lhs = - variable_lvalue_from_node(&assignment.nodes.0, syntax_tree, packed_dimensions); - let rhs = expr_from_expression_with_types( - &assignment.nodes.2, - syntax_tree, - packed_dimensions, - ); - match (lhs, rhs, op) { - (Some(lhs), Some(rhs), Some("=")) => vec![Assignment::new(lhs, rhs)], - (Some(lhs), Some(rhs), Some(op)) => { - assignment_op_expr(&lhs, op, rhs, packed_dimensions) - .map(|rhs| vec![Assignment::new(lhs, rhs)]) - .unwrap_or_default() - } - _ => Vec::new(), - } - } - _ => Vec::new(), - }, - sv_parser::StatementItem::SeqBlock(block) => block - .nodes - .3 - .iter() - .flat_map(|stmt| { - assignments_from_statement_or_null(stmt, syntax_tree, packed_dimensions) - }) - .collect(), - _ => Vec::new(), - } -} - -fn assignments_from_statement_or_null( - stmt: &sv_parser::StatementOrNull, - syntax_tree: &SyntaxTree, - packed_dimensions: &PackedDimensions, -) -> Vec { - match stmt { - sv_parser::StatementOrNull::Statement(stmt) => { - assignments_from_statement(stmt, syntax_tree, packed_dimensions) - } - sv_parser::StatementOrNull::Attribute(_) => Vec::new(), - } -} - fn assignment_op_expr( lhs: &LValue, op: &str, @@ -7832,7 +13128,20 @@ fn lvalue_from_select( let bit_selects = select.nodes.1.nodes.0.as_slice(); let indices = bit_selects .iter() - .map(|bit_select| const_expr_from_expr(&bit_select.nodes.1, syntax_tree)) + .map(|bit_select| { + let expr = expr_from_expression_with_types( + &bit_select.nodes.1, + syntax_tree, + packed_dimensions, + )?; + expr_to_lvalue_const(expand_expr_calls( + expr, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + true, + )) + }) .collect::>>()?; if let Some(range) = &select.nodes.2 { let sv_parser::PartSelectRange::ConstantRange(range) = &range.nodes.1 else { @@ -8166,7 +13475,10 @@ fn guard_zero_divisions(expr: Expr) -> Expr { op, right: right.clone(), }; - if matches!(op, BinaryOp::Div | BinaryOp::Mod) { + let divisor_is_nonzero = expr_to_const((*right).clone()) + .and_then(|right| eval_ast_const_expr(&right, &HashMap::default())) + .is_some_and(|right| right != 0); + if matches!(op, BinaryOp::Div | BinaryOp::Mod) && !divisor_is_nonzero { Expr::Mux { condition: Box::new(Expr::Binary { left: right, @@ -9258,23 +14570,118 @@ fn expr_to_const(expr: Expr) -> Option { op, right: Box::new(expr_to_const(*right)?), }), - Expr::Select { .. } - | Expr::Concat(_) - | Expr::RepeatConcat { .. } - | Expr::Resize { .. } - | Expr::Mux { .. } - | Expr::Call { .. } => None, + Expr::Select { expr, msb, lsb, .. } if msb == lsb => Some(ConstExpr::Select { + expr: Box::new(expr_to_const(*expr)?), + bit: Box::new(msb), + }), + Expr::Mux { + condition, + then_expr, + else_expr, + } => Some(ConstExpr::Mux { + condition: Box::new(expr_to_const(*condition)?), + then_expr: Box::new(expr_to_const(*then_expr)?), + else_expr: Box::new(expr_to_const(*else_expr)?), + }), + Expr::Call { name, args } => Some(ConstExpr::Function { + name, + args: args.into_iter().map(expr_to_const).collect::>()?, + }), + Expr::Select { .. } | Expr::Concat(_) | Expr::RepeatConcat { .. } | Expr::Resize { .. } => { + None + } + } +} + +fn expr_to_lvalue_const(expr: Expr) -> Option { + match expr { + Expr::Resize { + expr, + width, + signed, + } => { + let expr = expr_to_lvalue_const(*expr)?; + if width == 0 { + return Some(ConstExpr::Literal("0".to_string())); + } + if width == 1 && !signed { + return Some(ConstExpr::Select { + expr: Box::new(expr), + bit: Box::new(ConstExpr::Literal("0".to_string())), + }); + } + let mask = (num_bigint::BigUint::from(1u8) << width) - num_bigint::BigUint::from(1u8); + let truncated = ConstExpr::Binary { + left: Box::new(expr), + op: BinaryOp::BitAnd, + right: Box::new(ConstExpr::Literal(format!("{width}'h{mask:x}"))), + }; + if signed { + Some(ConstExpr::Mux { + condition: Box::new(ConstExpr::Select { + expr: Box::new(truncated.clone()), + bit: Box::new(ConstExpr::Literal((width - 1).to_string())), + }), + // Every negative packed index is out of range. A stable + // positive sentinel preserves that selection behavior + // without requiring a signed-resize node in ConstExpr. + then_expr: Box::new(ConstExpr::Literal(i128::MAX.to_string())), + else_expr: Box::new(truncated), + }) + } else { + Some(truncated) + } + } + Expr::Ident(name) => Some(ConstExpr::Ident(name)), + Expr::Literal(value) => Some(ConstExpr::Literal(value)), + Expr::Unary { op, expr } => Some(ConstExpr::Unary { + op, + expr: Box::new(expr_to_lvalue_const(*expr)?), + }), + Expr::Binary { left, op, right } => Some(ConstExpr::Binary { + left: Box::new(expr_to_lvalue_const(*left)?), + op, + right: Box::new(expr_to_lvalue_const(*right)?), + }), + Expr::Select { expr, msb, lsb, .. } if msb == lsb => Some(ConstExpr::Select { + expr: Box::new(expr_to_lvalue_const(*expr)?), + bit: Box::new(msb), + }), + Expr::Mux { + condition, + then_expr, + else_expr, + } => Some(ConstExpr::Mux { + condition: Box::new(expr_to_lvalue_const(*condition)?), + then_expr: Box::new(expr_to_lvalue_const(*then_expr)?), + else_expr: Box::new(expr_to_lvalue_const(*else_expr)?), + }), + Expr::Call { name, args } => Some(ConstExpr::Function { + name, + args: args + .into_iter() + .map(expr_to_lvalue_const) + .collect::>()?, + }), + Expr::Select { .. } | Expr::Concat(_) | Expr::RepeatConcat { .. } => None, } } -fn const_expr_from_constant_param( +fn const_expr_from_constant_param_with_env( expr: &sv_parser::ConstantParamExpression, syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { match expr { sv_parser::ConstantParamExpression::ConstantMintypmaxExpression(expr) => match &**expr { sv_parser::ConstantMintypmaxExpression::Unary(expr) => { - const_expr_from_ref_node(RefNode::ConstantExpression(expr), syntax_tree) + const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(expr), + syntax_tree, + const_env, + type_aliases, + ) } sv_parser::ConstantMintypmaxExpression::Ternary(_) => None, }, @@ -9315,6 +14722,15 @@ fn direction_from_port_direction(direction: &sv_parser::PortDirection) -> PortDi } fn type_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Option { + type_from_ref_node_with_env(node, syntax_tree, &HashMap::default(), &HashMap::default()) +} + +fn type_from_ref_node_with_env( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { if let Some(atom) = integer_atom_expr_type(node.clone()) { let kind = if integer_atom_is_2state(node.clone()) { TypeKind::Bit @@ -9340,7 +14756,8 @@ fn type_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Option) -> bool { fn type_from_net_port_header( header: &sv_parser::NetPortHeaderOrInterfacePortHeader, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Option { let sv_parser::NetPortHeaderOrInterfacePortHeader::NetPortHeader(header) = header else { @@ -9367,16 +14785,19 @@ fn type_from_net_port_header( match &header.nodes.1 { sv_parser::NetPortType::DataType(data_type) => match &data_type.nodes.1 { sv_parser::DataTypeOrImplicit::ImplicitDataType(_) => Some(Type::implicit()), - sv_parser::DataTypeOrImplicit::DataType(_) => { - type_from_ref_node(RefNode::DataTypeOrImplicit(&data_type.nodes.1), syntax_tree) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(&data_type.nodes.1), - syntax_tree, - type_aliases, - ) - }) - } + sv_parser::DataTypeOrImplicit::DataType(_) => type_from_ref_node_with_env( + RefNode::DataTypeOrImplicit(&data_type.nodes.1), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| { + type_alias_from_ref_node( + RefNode::DataTypeOrImplicit(&data_type.nodes.1), + syntax_tree, + type_aliases, + ) + }), }, sv_parser::NetPortType::NetTypeIdentifier(identifier) => { let name = identifier_text(RefNode::NetTypeIdentifier(identifier), syntax_tree)?; @@ -9389,25 +14810,28 @@ fn type_from_net_port_header( fn type_from_variable_port_header( header: &sv_parser::VariablePortHeader, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Option { match &header.nodes.1.nodes.0 { - sv_parser::VarDataType::DataType(data_type) => { - type_from_ref_node(RefNode::DataType(data_type), syntax_tree) - .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)) - } + sv_parser::VarDataType::DataType(data_type) => type_from_ref_node_with_env( + RefNode::DataType(data_type), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| type_alias_from_data_type(data_type, syntax_tree, type_aliases)), sv_parser::VarDataType::Var(data_type) => match &data_type.nodes.1 { sv_parser::DataTypeOrImplicit::ImplicitDataType(_) => Some(Type::implicit()), - sv_parser::DataTypeOrImplicit::DataType(_) => { - type_from_ref_node(RefNode::DataTypeOrImplicit(&data_type.nodes.1), syntax_tree) - .or_else(|| { - type_alias_from_data_type_or_implicit( - &data_type.nodes.1, - syntax_tree, - type_aliases, - ) - }) - } + sv_parser::DataTypeOrImplicit::DataType(_) => type_from_ref_node_with_env( + RefNode::DataTypeOrImplicit(&data_type.nodes.1), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| { + type_alias_from_data_type_or_implicit(&data_type.nodes.1, syntax_tree, type_aliases) + }), }, } } @@ -9440,15 +14864,20 @@ fn type_alias_from_data_type( type_aliases.get(&name).cloned() } -fn type_with_fallback_ranges( +fn type_with_fallback_ranges_with_env( mut r#type: Type, node: RefNode<'_>, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Type { - let direct_ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); + let direct_ranges = + packed_ranges_from_ref_node_with_env(node.clone(), syntax_tree, const_env, type_aliases); if type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases).is_some() { - r#type.packed_ranges.extend(direct_ranges); + // Use-site dimensions enclose the aliased packed type. + let mut ranges = direct_ranges; + ranges.extend(r#type.packed_ranges); + r#type.packed_ranges = ranges; } else if r#type.packed_ranges.is_empty() { r#type.packed_ranges = direct_ranges; } @@ -9475,18 +14904,27 @@ fn is_signed_from_ref_node(node: RefNode<'_>) -> Option { } } -fn packed_ranges_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Vec { +fn packed_ranges_from_ref_node_with_env( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Vec { let mut ranges = Vec::new(); for child in node { if let RefNode::PackedDimensionRange(range) = child { let constant_range = &range.nodes.0.nodes.1; - let left = const_expr_from_ref_node( + let left = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&constant_range.nodes.0), syntax_tree, + const_env, + type_aliases, ); - let right = const_expr_from_ref_node( + let right = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&constant_range.nodes.2), syntax_tree, + const_env, + type_aliases, ); if let (Some(left), Some(right)) = (left, right) { ranges.push(PackedRange::new(left, right)); @@ -9496,22 +14934,28 @@ fn packed_ranges_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> V ranges } -fn unpacked_ranges_from_dimensions( +fn unpacked_ranges_from_dimensions_with_env( dimensions: &[sv_parser::UnpackedDimension], syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Result, AnalyzerError> { dimensions .iter() .map(|dimension| match dimension { sv_parser::UnpackedDimension::Range(range) => { let constant_range = &range.nodes.0.nodes.1; - let left = const_expr_from_ref_node( + let left = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&constant_range.nodes.0), syntax_tree, + const_env, + type_aliases, ); - let right = const_expr_from_ref_node( + let right = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&constant_range.nodes.2), syntax_tree, + const_env, + type_aliases, ); match (left, right) { (Some(left), Some(right)) => Ok(UnpackedRange::new(left, right)), @@ -9521,9 +14965,11 @@ fn unpacked_ranges_from_dimensions( } } sv_parser::UnpackedDimension::Expression(expression) => { - let size = const_expr_from_ref_node( + let size = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&expression.nodes.0.nodes.1), syntax_tree, + const_env, + type_aliases, ); size.map(UnpackedRange::sized).ok_or_else(|| { AnalyzerError::Unsupported("unresolved unpacked array dimension".to_string()) @@ -9550,17 +14996,21 @@ fn validate_unpacked_dimension_sizes( Ok(()) } -fn unpacked_ranges_from_variable_dimensions( +fn unpacked_ranges_from_variable_dimensions_with_env( dimensions: &[sv_parser::VariableDimension], syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Result, AnalyzerError> { let mut ranges = Vec::new(); for dimension in dimensions { match dimension { sv_parser::VariableDimension::UnpackedDimension(dimension) => { - ranges.extend(unpacked_ranges_from_dimensions( + ranges.extend(unpacked_ranges_from_dimensions_with_env( std::slice::from_ref(&**dimension), syntax_tree, + const_env, + type_aliases, )?); } sv_parser::VariableDimension::UnsizedDimension(_) => { @@ -9584,16 +15034,32 @@ fn unpacked_ranges_from_variable_dimensions( } fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Option { + const_expr_from_ref_node_with_env(node, syntax_tree, &HashMap::default(), &HashMap::default()) +} + +fn const_expr_from_ref_node_with_env( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> Option { match node { RefNode::ConstantExpression(expr) => match expr { sv_parser::ConstantExpression::ConstantPrimary(primary) => { - const_expr_from_ref_node(RefNode::ConstantPrimary(primary), syntax_tree) + const_expr_from_ref_node_with_env( + RefNode::ConstantPrimary(primary), + syntax_tree, + const_env, + type_aliases, + ) } sv_parser::ConstantExpression::Unary(unary) => { let op = unary_op_from_symbol(&unary.nodes.0.nodes.0.nodes.0, syntax_tree)?; - let expr = const_expr_from_ref_node( + let expr = const_expr_from_ref_node_with_env( RefNode::ConstantPrimary(&unary.nodes.2), syntax_tree, + const_env, + type_aliases, )?; Some(ConstExpr::Unary { op, @@ -9602,14 +15068,18 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti } sv_parser::ConstantExpression::Binary(binary) => { let right_is_grouped = constant_expression_is_grouped(&binary.nodes.3); - let left = const_expr_from_ref_node( + let left = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&binary.nodes.0), syntax_tree, + const_env, + type_aliases, )?; let op = binary_op_from_symbol(&binary.nodes.1.nodes.0.nodes.0, syntax_tree)?; - let right = const_expr_from_ref_node( + let right = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&binary.nodes.3), syntax_tree, + const_env, + type_aliases, )?; let expr = ConstExpr::Binary { left: Box::new(left), @@ -9623,7 +15093,12 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti }) } sv_parser::ConstantExpression::Ternary(expr) => { - const_expr_from_constant_expression_ternary(expr, syntax_tree) + const_expr_from_constant_expression_ternary_with_env( + expr, + syntax_tree, + const_env, + type_aliases, + ) } sv_parser::ConstantExpression::Inside(_) => None, }, @@ -9640,10 +15115,26 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti let base = identifier_locate(identifier) .and_then(|locate| syntax_tree.get_str(&locate).map(str::to_string)) .map(ConstExpr::Ident)?; - const_select_expr(base.clone(), ¶meter.nodes.1, syntax_tree).or(Some(base)) + const_select_expr( + base.clone(), + ¶meter.nodes.1, + syntax_tree, + const_env, + type_aliases, + ) + .or(Some(base)) } sv_parser::ConstantPrimary::ConstantFunctionCall(call) => { - const_expr_from_function_subroutine_call(&call.nodes.0, syntax_tree).or_else(|| { + let lowered = const_expr_from_function_subroutine_call(&call.nodes.0, syntax_tree); + if let Some(ConstExpr::Function { name, args }) = &lowered + && name == "$bits" + && let [arg] = args.as_slice() + && let Some(r#type) = + infer_const_expr_type(arg, ¶meter_types_from_const_env(const_env)) + { + return Some(ConstExpr::Literal(r#type.width.to_string())); + } + lowered.or_else(|| { let sv_parser::SubroutineCall::TfCall(tf_call) = &call.nodes.0.nodes.0 else { return None; }; @@ -9661,12 +15152,16 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti }) } sv_parser::ConstantPrimary::ConstantCast(cast) => { - constant_cast_zero_type(cast, syntax_tree, &HashMap::default(), &HashMap::default()) - .map(|r#type| ConstExpr::Literal(typed_zero_literal(r#type))) + constant_cast_const_expr(cast, syntax_tree, const_env, type_aliases) } sv_parser::ConstantPrimary::MintypmaxExpression(expr) => match &expr.nodes.0.nodes.1 { sv_parser::ConstantMintypmaxExpression::Unary(expr) => { - const_expr_from_ref_node(RefNode::ConstantExpression(expr), syntax_tree) + const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(expr), + syntax_tree, + const_env, + type_aliases, + ) } sv_parser::ConstantMintypmaxExpression::Ternary(_) => None, }, @@ -9698,22 +15193,30 @@ fn constant_expression_is_grouped(expr: &sv_parser::ConstantExpression) -> bool ) } -fn const_expr_from_constant_expression_ternary( +fn const_expr_from_constant_expression_ternary_with_env( expr: &sv_parser::ConstantExpressionTernary, syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { Some(ConstExpr::Mux { - condition: Box::new(const_expr_from_ref_node( + condition: Box::new(const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&expr.nodes.0), syntax_tree, + const_env, + type_aliases, )?), - then_expr: Box::new(const_expr_from_ref_node( + then_expr: Box::new(const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&expr.nodes.3), syntax_tree, + const_env, + type_aliases, )?), - else_expr: Box::new(const_expr_from_ref_node( + else_expr: Box::new(const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&expr.nodes.5), syntax_tree, + const_env, + type_aliases, )?), }) } @@ -9722,14 +15225,18 @@ fn const_select_expr( base: ConstExpr, select: &sv_parser::ConstantSelect, syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { let bit_selects = select.nodes.1.nodes.0.as_slice(); if bit_selects.len() != 1 || select.nodes.2.is_some() { return None; } - let bit = const_expr_from_ref_node( + let bit = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&bit_selects[0].nodes.1), syntax_tree, + const_env, + type_aliases, )?; Some(ConstExpr::Select { expr: Box::new(base), diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 9bcadcd6e..41b71aff1 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -155,6 +155,2342 @@ mod tests { } } + #[test] + fn keeps_case_item_guards_on_nested_comb_branches() { + let ir = analyze_source( + r#" + module Top(input logic s, t, a, b, c, output logic y); + always_comb begin + case (s) + 1'b0: if (t) y = a; else y = b; + default: y = c; + endcase + end + endmodule + "#, + Path::new("nested_case.sv"), + ) + .expect("SV analysis should succeed"); + + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + assert_eq!(assignments.len(), 1); + let ir::Expr::Mux { else_expr, .. } = assignments[0].rhs() else { + panic!("expected a multiplexer chain"); + }; + // The default branch value must remain the final fallback so that + // `s != 0` selects `c`, not the nested else value. + assert_eq!(expr_bottom_else(else_expr), "c"); + } + + fn expr_bottom_else(expr: &ir::Expr) -> String { + match expr { + ir::Expr::Mux { else_expr, .. } => expr_bottom_else(else_expr), + ir::Expr::Ident(name) => name.clone(), + other => panic!("unexpected expression in mux chain: {other:?}"), + } + } + + #[test] + fn preserves_reads_between_merged_conditional_writes() { + let ir = analyze_source( + r#" + module Top(input logic c, d, output logic x, y); + always_comb begin + x = d; + y = x; + if (c) x = 1'b1; + end + endmodule + "#, + Path::new("intervening_read.sv"), + ) + .expect("intervening read should use the value at its statement position"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let y = assignments + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!( + expr_references_ident_name(y.rhs(), "d"), + "expected y to use the preceding d assignment: {:?}", + y.rhs() + ); + assert!(!expr_references_ident_name(y.rhs(), "x")); + } + + #[test] + fn snapshots_unconditional_sources_before_relocated_conditional_writes() { + let ir = analyze_source( + r#" + module Top(input logic a, b, c, d, e, output logic x, y); + always_comb begin + y = a; + if (c) x = y; + else x = b; + y = d; + if (e) x = b; + end + endmodule + "#, + Path::new("relocated_cross_target_read.sv"), + ) + .expect("a relocated write should retain values read at its source position"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let x = assignments + .iter() + .find(|assignment| assignment.lhs() == "x") + .expect("x assignment"); + assert!(expr_references_ident_name(x.rhs(), "a")); + assert!( + !expr_references_ident_name(x.rhs(), "y"), + "x must snapshot y before its later overwrite: {:?}", + x.rhs() + ); + } + + #[test] + fn preserves_fallback_guards_for_each_comb_target() { + let ir = analyze_source( + r#" + module Top(input logic c, a, b, output logic x, y); + always_comb begin + x = 1'b0; + y = 1'b0; + if (c) x = a; + else y = b; + end + endmodule + "#, + Path::new("target_fallback.sv"), + ) + .expect("SV analysis should succeed"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let y = assignments + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!( + matches!(y.rhs(), ir::Expr::Mux { .. }), + "the else write must not become globally unconditional: {:?}", + y.rhs() + ); + } + + #[test] + fn keeps_writes_after_exhaustive_comb_fallbacks() { + let ir = analyze_source( + r#" + module Top(input logic c, d, a, b, e, output logic x); + always_comb begin + if (c) x = a; + else x = b; + if (d) x = e; + end + endmodule + "#, + Path::new("write_after_fallback.sv"), + ) + .expect("SV analysis should succeed"); + let rhs = ir.modules()[0].comb_processes()[0].assignments()[0].rhs(); + let ir::Expr::Mux { condition, .. } = rhs else { + panic!("expected the trailing write to produce a mux: {rhs:?}"); + }; + assert!( + expr_references_ident_name(condition, "d"), + "the trailing d write must retain priority: {rhs:?}" + ); + } + + #[test] + fn later_exhaustive_comb_chain_overrides_the_previous_chain() { + let ir = analyze_source( + r#" + module Top(input logic c, d, a, b, e, f, output logic x); + always_comb begin + if (c) x = a; + else x = b; + if (d) x = e; + else x = f; + end + endmodule + "#, + Path::new("consecutive_exhaustive_chains.sv"), + ) + .expect("the later exhaustive chain should fully define x"); + let rhs = ir.modules()[0].comb_processes()[0].assignments()[0].rhs(); + assert!(expr_references_ident_name(rhs, "d")); + assert!(expr_references_ident_name(rhs, "e")); + assert!(expr_references_ident_name(rhs, "f")); + assert!( + !expr_references_ident_name(rhs, "c") + && !expr_references_ident_name(rhs, "a") + && !expr_references_ident_name(rhs, "b"), + "the fully overriding second chain must discard the first chain: {rhs:?}" + ); + } + + #[test] + fn recognizes_complementary_equality_guards_as_exhaustive() { + analyze_source( + r#" + module Top(input logic outer, input bit s, input logic a, b, c, output logic y); + always_comb begin + if (outer) begin + if (s == 0) y = a; + if (s != 0) y = b; + end else begin + y = c; + end + end + endmodule + "#, + Path::new("complementary_equality_guards.sv"), + ) + .expect("complementary two-state equality guards should define y exhaustively"); + } + + #[test] + fn recognizes_block_wrapped_complementary_guards_as_exhaustive() { + analyze_source( + r#" + module Top(input logic outer, input bit s, input logic a, b, c, output logic y); + always_comb begin + if (outer) begin + if (s) begin y = a; end + if (!s) begin y = b; end + end else begin + y = c; + end + end + endmodule + "#, + Path::new("block_wrapped_complementary_guards.sv"), + ) + .expect("block-wrapped complementary guards should define y exhaustively"); + } + + #[test] + fn preserves_complementary_guards_across_harmless_blocks() { + analyze_source( + r#" + module Top(input logic outer, input bit s, input logic a, b, output logic y, z); + always_comb begin + if (outer) begin + if (s) y = a; + begin z = 1'b0; end + if (!s) y = b; + end else begin + y = a; + z = 1'b1; + end + end + endmodule + "#, + Path::new("harmless_block_between_complementary_guards.sv"), + ) + .expect("a block that cannot change the guard should preserve its proof"); + } + + #[test] + fn invalidates_complementary_guards_for_overlapping_selected_writes() { + let error = analyze_source( + r#" + module Top( + input logic outer, q, a, b, c, + input bit idx, + output logic y + ); + logic [1:0] s; + always_comb begin + s = {q, q}; + if (outer) begin + if (s[0]) y = a; + s[idx] = b; + if (!s[0]) y = c; + end else begin + y = a; + end + end + endmodule + "#, + Path::new("overlapping_write_between_complementary_guards.sv"), + ) + .expect_err("a dynamic overlapping write must invalidate the guard proof"); + assert!( + error + .to_string() + .contains("latch inference inside always_comb") + ); + + let error = analyze_source( + r#" + module Top(input logic outer, q, a, b, output logic y); + logic s; + function automatic bit f(); + return s; + endfunction + always_comb begin + s = q; + if (outer) begin + if (f()) y = a; + s = 1'b1; + if (!f()) y = b; + end else begin + y = a; + end + end + endmodule + "#, + Path::new("function_guard_dependency_write.sv"), + ) + .expect_err("a function guard's free-variable write must invalidate its proof"); + assert!( + error + .to_string() + .contains("latch inference inside always_comb") + ); + } + + #[test] + fn substitutes_reads_of_selected_comb_targets() { + let ir = analyze_source( + r#" + module Top(input logic c, a, b, output logic [1:0] x, output logic y); + always_comb begin + x[0] = a; + y = x[0]; + if (c) x[0] = b; + end + endmodule + "#, + Path::new("selected_intervening_read.sv"), + ) + .expect("SV analysis should succeed"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let y = assignments + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!(expr_references_ident_name(y.rhs(), "a")); + assert!( + !expr_references_ident_name(y.rhs(), "x"), + "y must observe the preceding selected write: {:?}", + y.rhs() + ); + } + + #[test] + fn substitutes_subselect_reads_of_selected_comb_targets() { + let ir = analyze_source( + r#" + module Top( + input logic c, + input logic [2:0] a, b, + output logic [3:0] x, + output logic y + ); + always_comb begin + x[3:1] = a; + y = x[2]; + if (c) x[3:1] = b; + end + endmodule + "#, + Path::new("selected_subselect_intervening_read.sv"), + ) + .expect("SV analysis should succeed"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let y = assignments + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!(expr_references_ident_name(y.rhs(), "a")); + let ir::Expr::Select { msb, lsb, .. } = y.rhs() else { + panic!( + "expected the intervening bit read to select from a: {:?}", + y.rhs() + ); + }; + assert_eq!(msb, &ir::ConstExpr::Literal("1".to_string())); + assert_eq!(lsb, &ir::ConstExpr::Literal("1".to_string())); + assert!( + !expr_references_ident_name(y.rhs(), "x"), + "y must observe the matching bit of the preceding selected write: {:?}", + y.rhs() + ); + } + + #[test] + fn substitutes_partially_overlapping_reads_of_selected_comb_targets() { + let ir = analyze_source( + r#" + module Top( + input logic c, + input logic [2:0] a, b, + output logic [4:0] x, + output logic [2:0] y + ); + always_comb begin + x[3:1] = a; + y = x[4:2]; + if (c) x[3:1] = b; + end + endmodule + "#, + Path::new("selected_partial_overlap_intervening_read.sv"), + ) + .expect("SV analysis should succeed"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let y = assignments + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!( + expr_references_ident_name(y.rhs(), "a"), + "the overlapping bits must come from the preceding selected write: {:?}", + y.rhs() + ); + assert!( + expr_references_ident_name(y.rhs(), "x"), + "the non-overlapping bit must retain its original source: {:?}", + y.rhs() + ); + } + + #[test] + fn coerces_always_comb_if_predicates_to_procedural_truth() { + let ir = analyze_source( + r#" + module Top(input logic s, output logic y); + always_comb begin + if (s) y = 1'b1; + else y = 1'b0; + end + endmodule + "#, + Path::new("always_comb_procedural_truth.sv"), + ) + .expect("SV analysis should succeed"); + let rhs = ir.modules()[0].comb_processes()[0].assignments()[0].rhs(); + let ir::Expr::Mux { condition, .. } = rhs else { + panic!("expected conditional assignment mux: {rhs:?}"); + }; + assert!(matches!( + &**condition, + ir::Expr::Unary { + op: ir::UnaryOp::RedOr, + expr, + } if matches!( + &**expr, + ir::Expr::Unary { + op: ir::UnaryOp::ToTwoState, + .. + } + ) + )); + } + + #[test] + fn applies_cross_target_substitutions_before_merging_comb_groups() { + let ir = analyze_source( + r#" + module Top(input logic c, d, output logic x, y); + always_comb begin + y = 1'b0; + x = 1'b0; + y = x; + if (c) x = 1'b1; + if (d) y = 1'b1; + end + endmodule + "#, + Path::new("cross_target_substitution.sv"), + ) + .expect("SV analysis should succeed"); + let y = ir.modules()[0].comb_processes()[0] + .assignments() + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!( + !expr_references_ident_name(y.rhs(), "x"), + "y must use x's value at the intervening statement: {:?}", + y.rhs() + ); + } + + #[test] + fn uses_whole_vector_defaults_for_conditional_selected_writes() { + let ir = analyze_source( + r#" + module Top(input logic c, output logic [1:0] x); + always_comb begin + x = '0; + if (c) x[0] = 1'b1; + end + endmodule + "#, + Path::new("whole_then_selected.sv"), + ) + .expect("whole-vector initialization should cover the selected fallback"); + assert_eq!(ir.modules()[0].comb_processes()[0].assignments().len(), 2); + } + + #[test] + fn uses_selected_writes_before_conditional_whole_vector_writes() { + analyze_source( + r#" + module Top(input logic c, a, output logic [7:0] x); + always_comb begin + x = '0; + x[0] = a; + if (c) x = 8'hff; + end + endmodule + "#, + Path::new("selected_then_conditional_whole.sv"), + ) + .expect("the preceding selected write should initialize the whole-write fallback"); + } + + #[test] + fn permits_reads_after_assignments_on_the_same_comb_path() { + analyze_source( + r#" + module Top(input logic c, output logic x, y); + always_comb begin + if (c) begin + x = 1'b1; + y = x; + end else begin + x = 1'b0; + y = x; + end + end + endmodule + "#, + Path::new("path_local_comb_read.sv"), + ) + .expect("each guarded read is preceded by a write on the same path"); + } + + #[test] + fn freezes_comb_branch_guards_before_overwriting_the_predicate() { + let ir = analyze_source( + r#" + module Top(input logic en, output logic t, y); + always_comb begin + t = en; + y = 1'b0; + if (t) begin + t = 1'b0; + y = 1'b1; + end + end + endmodule + "#, + Path::new("frozen_comb_guard.sv"), + ) + .expect("the branch predicate should use t's value on entry"); + let y = ir.modules()[0].comb_processes()[0] + .assignments() + .iter() + .find(|assignment| assignment.lhs() == "y") + .expect("y assignment"); + assert!(expr_references_ident_name(y.rhs(), "en")); + assert!(!expr_references_ident_name(y.rhs(), "t")); + } + + #[test] + fn rejects_entry_value_guards_relocated_past_their_first_write() { + let error = analyze_source( + r#" + module Top( + input logic a, b, d, e, + output logic t, x + ); + always_comb begin + if (t) x = a; + else x = b; + t = d; + if (e) x = b; + end + endmodule + "#, + Path::new("relocated_entry_guard.sv"), + ) + .expect_err("the entry value of t cannot be moved past t's first write"); + assert!( + error + .to_string() + .contains("read-before-write dependency inside always_comb"), + "unexpected error: {error}" + ); + + let error = analyze_source( + r#" + module Top( + input logic a, b, c, d, e, f, + output logic y, + output logic [1:0] x + ); + always_comb begin + y = 1'b0; + if (x) y = a; + x[0] = c; + if (d) x[0] = e; + if (f) y = b; + end + endmodule + "#, + Path::new("relocated_overlapping_entry_guard.sv"), + ) + .expect_err("a whole-vector guard read cannot move past a selected write"); + assert!( + error + .to_string() + .contains("read-before-write dependency inside always_comb"), + "unexpected error: {error}" + ); + } + + #[test] + fn preserves_prior_partially_overlapping_selected_writes() { + let ir = analyze_source( + r#" + module Top( + input logic c, d, + input logic [2:0] a, b, + output logic [3:0] x + ); + always_comb begin + x = '0; + if (c) x[3:1] = a; + if (d) x[2:0] = b; + end + endmodule + "#, + Path::new("overlapping_selected_fallback.sv"), + ) + .expect("overlapping selected writes should preserve their procedural order"); + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + let rhs = assignments.last().expect("final selected assignment").rhs(); + assert!( + expr_references_ident_name(rhs, "a"), + "the later false path must retain the earlier selected value: {rhs:?}" + ); + } + + #[test] + fn requires_definite_assignment_before_filling_comb_fallbacks() { + let error = analyze_source( + r#" + module Top(input logic c, d, a, b, output logic x); + always_comb begin + if (c) x = a; + else if (d) x = b; + end + endmodule + "#, + Path::new("nested_incomplete_fallback.sv"), + ) + .expect_err("the incomplete nested fallback must infer a latch") + .to_string(); + assert!(error.contains("latch inference inside always_comb")); + } + + #[test] + fn rejects_genuine_self_reads_in_exhaustive_comb_branches() { + let error = analyze_source( + r#" + module Top(input logic c, output logic [7:0] x); + always_comb begin + if (c) x = x + 1; + else x = 0; + end + endmodule + "#, + Path::new("genuine_self_read.sv"), + ) + .expect_err("a genuine self-read must not be filled as a fallback hole") + .to_string(); + assert!(error.contains("latch inference inside always_comb")); + } + + #[test] + fn rejects_overlapping_selected_self_reads() { + let error = analyze_source( + r#" + module Top(input logic c, output logic [1:0] x); + always_comb begin + if (c) x[0] = x[1:0]; + else x[0] = 1'b0; + end + endmodule + "#, + Path::new("overlapping_selected_self_read.sv"), + ) + .expect_err("an overlapping selected self-read must be rejected") + .to_string(); + assert!(error.contains("latch inference inside always_comb")); + } + + #[test] + fn sign_extends_negative_literals_in_widening_constant_casts() { + let ir = analyze_source( + r#" + module Top #(parameter V = $bits(logic signed [7:0])'(4'shf)) (); + endmodule + "#, + Path::new("sign_extend_cast.sv"), + ) + .expect("SV analysis should succeed"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(-1)); + } + + #[test] + fn sizes_first_dimension_for_size_cast_targets() { + let ir = analyze_source( + r#" + module Top #( + parameter W = $size(logic [1:0][3:0])'(3'd7), + parameter B = $bits(logic [1:0][3:0])'(4'd7) + ) (); + endmodule + "#, + Path::new("size_cast.sv"), + ) + .expect("SV analysis should succeed"); + // A 2-bit $size target truncates 7 to 3; an 8-bit $bits target + // keeps it. + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(3)); + assert_eq!(ir.modules()[0].parameters()[1].resolved_value(), Some(7)); + } + + #[test] + fn infers_size_cast_targets_from_selected_expressions() { + let ir = analyze_source( + r#" + module Top(input logic [7:0] a); + localparam Q = $bits(a[3:0])'(4'hf); + localparam S = $size(a[3:0])'(4'hf); + endmodule + "#, + Path::new("selected_expression_size_cast.sv"), + ) + .expect("selected expression types should determine size cast widths"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(15)); + assert_eq!(ir.modules()[0].parameters()[1].resolved_value(), Some(15)); + } + + #[test] + fn treats_scalar_size_cast_targets_as_one_bit() { + let ir = analyze_source( + r#" + module Top #(parameter W = $size(logic)'(2'd3)) (); + endmodule + "#, + Path::new("scalar_size_cast.sv"), + ) + .expect("a scalar $size cast target should resolve to one bit"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(1)); + } + + #[test] + fn resolves_constant_cast_targets_from_module_environments() { + let alias_ir = analyze_source( + r#" + module Top; + typedef logic [7:0] byte_t; + localparam P = byte_t'(4'd3); + endmodule + "#, + Path::new("constant_alias_cast_env.sv"), + ) + .expect("typedef cast target should resolve"); + assert_eq!( + alias_ir.modules()[0].parameters()[0].resolved_value(), + Some(3) + ); + + let width_ir = analyze_source( + r#" + module Top; + localparam W = 8; + localparam Q = W'(4'd3); + endmodule + "#, + Path::new("constant_width_cast_env.sv"), + ) + .expect("parameter-sized cast target should resolve"); + assert_eq!( + width_ir.modules()[0].parameters()[1].resolved_value(), + Some(3) + ); + } + + #[test] + fn evaluates_constant_cast_operand_expressions() { + let ir = analyze_source( + r#" + module Top; + typedef logic [7:0] byte_t; + localparam A = 3; + localparam B = byte_t'(A); + localparam C = byte_t'(1 + 2); + endmodule + "#, + Path::new("constant_cast_operands.sv"), + ) + .expect("constant cast operands should be evaluated in the module environment"); + assert_eq!(ir.modules()[0].parameters()[1].resolved_value(), Some(3)); + assert_eq!(ir.modules()[0].parameters()[2].resolved_value(), Some(3)); + } + + #[test] + fn resolves_enum_members_referencing_earlier_members() { + let ir = analyze_source( + r#" + module Top(input logic [1:0] sel, output logic y); + typedef enum logic [1:0] { A = 2'd0, B = A + 2'd1 } E; + always_comb y = (sel == B); + endmodule + "#, + Path::new("enum_member_ref.sv"), + ) + .expect("SV analysis should succeed"); + + let assignments = ir.modules()[0].comb_processes()[0].assignments(); + assert_eq!(assignments.len(), 1); + // `B` must resolve to its constant value even though it references + // the earlier member `A`. + assert!( + !expr_references_ident_name(assignments[0].rhs(), "B"), + "unresolved enum member in {:?}", + assignments[0].rhs() + ); + assert!( + expr_contains_literal(assignments[0].rhs(), "1"), + "expected the folded member value in {:?}", + assignments[0].rhs() + ); + } + + #[test] + fn context_sizes_unbased_enum_member_initializers() { + let ir = analyze_source( + r#" + module Top; + typedef enum logic [3:0] { A = '1 } E; + logic [A:0] data; + endmodule + "#, + Path::new("unbased_enum_initializer.sv"), + ) + .expect("an unbased fill should be sized to the enum base type"); + let width = ir.modules()[0] + .signals() + .iter() + .find(|signal| signal.name() == "data") + .and_then(|signal| signal.r#type().resolved_width()); + assert_eq!(width, Some(16)); + } + + #[test] + fn preserves_enum_base_types_during_constant_substitution() { + let ir = analyze_source( + r#" + module Top(output logic [31:0] y); + typedef enum logic [1:0] { A = 2'd0 } E; + assign y = ~A; + endmodule + "#, + Path::new("typed_enum_constant.sv"), + ) + .expect("SV analysis should succeed"); + let rhs = ir.modules()[0].assignments()[0].rhs(); + let ir::Expr::Unary { expr, .. } = rhs else { + panic!("expected enum complement: {rhs:?}"); + }; + assert_eq!(&**expr, &ir::Expr::Literal("2'd0".to_string())); + } + + #[test] + fn rejects_casez_nested_under_comb_conditionals() { + let error = analyze_source( + r#" + module Top(input logic en, sel, output logic y); + always_comb begin + y = 1'b0; + if (en) casez (sel) + 1'b?: y = 1'b1; + endcase + end + endmodule + "#, + Path::new("nested_casez.sv"), + ) + .expect_err("nested casez must be rejected") + .to_string(); + assert!( + error.contains("casez or casex inside always_comb"), + "unexpected error: {error}" + ); + } + + fn expr_references_ident_name(expr: &ir::Expr, name: &str) -> bool { + match expr { + ir::Expr::Ident(ident) => ident == name, + ir::Expr::Select { expr, .. } => expr_references_ident_name(expr, name), + ir::Expr::Concat(parts) | ir::Expr::RepeatConcat { parts, .. } => parts + .iter() + .any(|part| expr_references_ident_name(part, name)), + ir::Expr::Resize { expr, .. } | ir::Expr::Unary { expr, .. } => { + expr_references_ident_name(expr, name) + } + ir::Expr::Call { args, .. } => { + args.iter().any(|arg| expr_references_ident_name(arg, name)) + } + ir::Expr::Binary { left, right, .. } => { + expr_references_ident_name(left, name) || expr_references_ident_name(right, name) + } + ir::Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_references_ident_name(condition, name) + || expr_references_ident_name(then_expr, name) + || expr_references_ident_name(else_expr, name) + } + ir::Expr::Literal(_) => false, + } + } + + fn expr_contains_literal(expr: &ir::Expr, needle: &str) -> bool { + match expr { + ir::Expr::Literal(value) => value == needle || value.ends_with(&format!("d{needle}")), + ir::Expr::Select { expr, .. } => expr_contains_literal(expr, needle), + ir::Expr::Concat(parts) | ir::Expr::RepeatConcat { parts, .. } => { + parts.iter().any(|part| expr_contains_literal(part, needle)) + } + ir::Expr::Resize { expr, .. } | ir::Expr::Unary { expr, .. } => { + expr_contains_literal(expr, needle) + } + ir::Expr::Call { args, .. } => { + args.iter().any(|arg| expr_contains_literal(arg, needle)) + } + ir::Expr::Binary { left, right, .. } => { + expr_contains_literal(left, needle) || expr_contains_literal(right, needle) + } + ir::Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_contains_literal(condition, needle) + || expr_contains_literal(then_expr, needle) + || expr_contains_literal(else_expr, needle) + } + ir::Expr::Ident(_) => false, + } + } + + #[test] + fn uses_enum_members_as_module_constants() { + let ir = analyze_source( + r#" + module Top #( + parameter logic [1:0] BASE = 2'd1 + ) (input logic a, output logic y); + typedef enum logic [1:0] { N = BASE + 2'd1 } E; + logic [N-1:0] data; + always_comb begin + if (N != 0) y = a; + else y = 1'b0; + end + endmodule + "#, + Path::new("enum_const_env.sv"), + ) + .expect("SV analysis should succeed"); + let width = ir.modules()[0] + .signals() + .iter() + .find(|signal| signal.name() == "data") + .map(|signal| signal.r#type().resolved_width()) + .unwrap(); + assert_eq!(width, Some(2)); + } + + #[test] + fn resolves_parameters_that_reference_enum_members() { + let ir = analyze_source( + r#" + module Top; + typedef enum logic [1:0] { N = 2 } E; + localparam W = N; + logic [W-1:0] data; + endmodule + "#, + Path::new("enum_parameter_dependency.sv"), + ) + .expect("parameters should be re-resolved after enum collection"); + let width = ir.modules()[0] + .signals() + .iter() + .find(|signal| signal.name() == "data") + .and_then(|signal| signal.r#type().resolved_width()); + assert_eq!(width, Some(2)); + } + + #[test] + fn re_resolves_parameters_between_enum_declarations() { + let ir = analyze_source( + r#" + module Top; + typedef enum logic [1:0] { A = 2 } E0; + localparam W = A + 1; + typedef enum logic [3:0] { B = W } E1; + logic [B-1:0] data; + endmodule + "#, + Path::new("enum_parameter_enum_dependency.sv"), + ) + .expect("parameters between enum declarations should be available to later enums"); + let width = ir.modules()[0] + .signals() + .iter() + .find(|signal| signal.name() == "data") + .and_then(|signal| signal.r#type().resolved_width()); + assert_eq!(width, Some(3)); + } + + #[test] + fn rebuilds_typedefs_after_preceding_enum_members() { + let ir = analyze_source( + r#" + module Top; + typedef enum int { W = 3 } E; + typedef logic [W'(2):0] B; + typedef enum B { A = 3'b101 } F; + logic [A-1:0] data; + endmodule + "#, + Path::new("enum_dependent_typedef.sv"), + ) + .expect("later enum bases should use typedefs rebuilt from preceding members"); + let width = ir.modules()[0] + .signals() + .iter() + .find(|signal| signal.name() == "data") + .and_then(|signal| signal.r#type().resolved_width()); + assert_eq!(width, Some(5)); + } + + #[test] + fn treats_constant_equivalent_mux_case_selectors_as_two_state() { + analyze_source( + r#" + module Top(input logic c, a, output logic y); + localparam logic P = 0; + localparam logic Q = 0; + always_comb begin + case (c ? P : Q) + 1'b0: y = a; + endcase + end + endmodule + "#, + Path::new("constant_equivalent_mux_case.sv"), + ) + .expect("equal constant mux arms should make the case selector exhaustive"); + } + + #[test] + fn skips_unreachable_sparse_case_items_with_default() { + analyze_source( + r#" + module Top(input bit [1:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 2'd0: y = a; + 2'd0: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("sparse_case_duplicate_with_default.sv"), + ) + .expect("unreachable duplicate items should not prevent definite assignment"); + } + + #[test] + fn skips_duplicate_case_items_for_four_state_selectors() { + analyze_source( + r#" + module Top(input logic s, input logic a, b, output logic y); + always_comb begin + case (s) + 1'b0: y = a; + 1'b0: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("four_state_duplicate_case_item.sv"), + ) + .expect("an unreachable duplicate case item should not infer a latch"); + } + + #[test] + fn compares_normalized_four_state_case_labels_for_reachability() { + analyze_source( + r#" + module Top(input logic [1:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 2'd0: y = a; + 2'b00: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("normalized_four_state_case_labels.sv"), + ) + .expect("equivalent case-label values should make the later item unreachable"); + + analyze_source( + r#" + module Top(input logic [8:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 9'd0: y = a; + 9'b000000000: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("wide_normalized_four_state_case_labels.sv"), + ) + .expect("equivalent wide labels should be normalized without domain enumeration"); + + analyze_source( + r#" + module Top(input logic signed [8:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 1'sb1: y = a; + 9'b111111111: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("selector_typed_four_state_case_labels.sv"), + ) + .expect("labels should be normalized in the signed selector context"); + + let error = analyze_source( + r#" + module Top(input logic [8:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 10'h3ff: y = a; + 9'h1ff: ; + default: y = b; + endcase + end + endmodule + "#, + Path::new("wider_unreachable_case_label.sv"), + ) + .expect_err("a wider unreachable label must not hide a reachable empty item"); + assert!( + error + .to_string() + .contains("latch inference inside always_comb") + ); + } + + #[test] + fn folds_compound_four_state_constant_case_selectors_for_coverage() { + analyze_source( + r#" + module Top(input logic c, a, b, output logic y); + always_comb begin + case (1'bx | 1'b0) + 1'bx: if (c) y = a; else y = b; + endcase + end + endmodule + "#, + Path::new("compound_four_state_constant_case_selector.sv"), + ) + .expect("a compound constant X selector should preserve its mask for coverage"); + } + + #[test] + fn expands_constant_function_predicates_for_definite_assignments() { + analyze_source( + r#" + module Top(input logic outer, a, b, output logic y); + function automatic bit one(); + return 1'b1; + endfunction + always_comb begin + if (outer) begin + if (one()) y = a; + end else begin + y = b; + end + end + endmodule + "#, + Path::new("constant_function_definite_assignment.sv"), + ) + .expect("a constant-true function predicate should make the inner write definite"); + } + + #[test] + fn folds_concatenated_four_state_constant_case_selectors_for_coverage() { + analyze_source( + r#" + module Top(input logic a, output logic y); + always_comb begin + case ({1'bx}) + 1'bx: y = a; + endcase + end + endmodule + "#, + Path::new("concatenated_four_state_constant_case_selector.sv"), + ) + .expect("a constant concatenation should retain its X mask for case coverage"); + } + + #[test] + fn recognizes_mixed_boolean_and_zero_equality_complements() { + analyze_source( + r#" + module Top(input logic outer, input bit s, input logic a, b, c, output logic y); + always_comb begin + if (outer) begin + if (s) y = a; + if (s == 0) y = b; + end else begin + y = c; + end + end + endmodule + "#, + Path::new("mixed_boolean_equality_complements.sv"), + ) + .expect("a two-state predicate and its zero equality should be complementary"); + } + + #[test] + fn resolves_function_types_in_size_cast_targets() { + let ir = analyze_source( + r#" + module Top(output logic [7:0] y); + function automatic logic [7:0] f(); + return 8'h00; + endfunction + localparam P = $bits(f())'(16'hffff); + always_comb y = P; + endmodule + "#, + Path::new("function_size_cast_target.sv"), + ) + .expect("a size-function cast target should use the function return type"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(0xff)); + + let ir = analyze_source( + r#" + module Top(output logic [7:0] y); + function automatic logic [$bits(g())'(7):0] f(); + return 8'h00; + endfunction + function automatic logic [7:0] g(); + return 8'h00; + endfunction + localparam P = $bits(f())'(16'hffff); + always_comb y = P; + endmodule + "#, + Path::new("dependent_function_size_cast_target.sv"), + ) + .expect("function return metadata discovery should resolve dependencies without recursion"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(0xff)); + } + + #[test] + fn preserves_function_return_dimensions_in_size_cast_targets() { + let ir = analyze_source( + r#" + module Top(output logic [1:0] y); + function automatic logic [1:0][3:0] f(); + return '0; + endfunction + localparam P = $size(f())'(8'hff); + always_comb y = P; + endmodule + "#, + Path::new("function_dimension_size_cast_target.sv"), + ) + .expect("$size should use the first packed function return dimension"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(3)); + + let ir = analyze_source( + r#" + module Top(output logic [1:0] y); + function automatic logic [$bits(g())'(1):0][3:0] f(); + return '0; + endfunction + function automatic logic [7:0] g(); + return '0; + endfunction + localparam P = $size(f())'(8'hff); + always_comb y = P; + endmodule + "#, + Path::new("dependent_function_dimension_size_cast_target.sv"), + ) + .expect("dependent function return dimensions should remain available to $size"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(3)); + } + + #[test] + fn substitutes_loop_indices_when_tracking_comb_writes() { + analyze_source( + r#" + module Top(input logic outer, q, a, b, c, output logic y); + logic [1:0] s; + always_comb begin + s = {q, q}; + if (outer) begin + if (s[0] === 1'b1) y = a; + for (int i = 1; i < 2; i++) s[i] = b; + if (s[0] !== 1'b1) y = c; + end else begin + y = a; + end + end + endmodule + "#, + Path::new("indexed_loop_comb_writes.sv"), + ) + .expect("a concrete nonoverlapping loop write should preserve the guard proof"); + } + + #[test] + fn rejects_static_for_loops_that_write_their_index() { + let error = analyze_source( + r#" + module Top(output logic [3:0] y); + always_comb begin + for (int i = 0; i < 4; i++) begin + y[i] = 1'b1; + i = i + 1; + end + end + endmodule + "#, + Path::new("loop_body_index_write.sv"), + ) + .expect_err("a loop body that changes its index must not be statically unrolled") + .to_string(); + assert!(error.contains("procedural loop inside always_comb")); + } + + #[test] + fn analyzes_comb_processes_with_generate_local_constants() { + analyze_source( + r#" + module Top(input logic a, output logic y); + if (1) begin : selected + localparam bit S = 1'b0; + always_comb begin + case (S) + 1'b0: y = a; + endcase + end + end + endmodule + "#, + Path::new("generate_local_comb_constant.sv"), + ) + .expect("generate-local constants should participate in always_comb analysis"); + } + + #[test] + fn folds_four_state_conditional_case_selectors_for_coverage() { + analyze_source( + r#" + module Top(input logic a, output logic y); + always_comb begin + case (1'bx ? 1'b0 : 1'b1) + 1'bx: y = a; + endcase + end + endmodule + "#, + Path::new("conditional_four_state_case_selector.sv"), + ) + .expect("a constant conditional selector should retain its merged X mask"); + } + + #[test] + fn types_unpacked_array_elements_in_size_cast_targets() { + let ir = analyze_source( + r#" + module Top(output logic [7:0] y); + logic [7:0] a[2]; + localparam P = $bits(a[0])'(16'hffff); + always_comb y = P; + endmodule + "#, + Path::new("array_element_size_cast_target.sv"), + ) + .expect("size-function expression typing should include module variable dimensions"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(0xff)); + } + + #[test] + fn preserves_logical_constant_case_selector_masks() { + for selector in [ + "1'bx && 1'b1", + "1'b0 || 1'bz", + "(1'bx && 1'b1) || 1'b0", + "!(1'bx || 1'b0)", + ] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({selector}) 1'bx: y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("logical_constant_case.sv")) + .unwrap_or_else(|error| panic!("{selector}: {error}")); + } + } + + #[test] + fn preserves_four_state_equality_case_selector_masks() { + for (selector, expected) in [ + ("1'bx == 1'bx", "1'bx"), + ("1'bz != 1'b0", "1'bx"), + ("2'b0x == 2'b1x", "1'b0"), + ("2'b0z != 2'b1x", "1'b1"), + ("!(1'bx == 1'b0)", "1'bx"), + ("(1'bx != 1'bz) && 1'b1", "1'bx"), + ("(1'bx && 1'b1) == 1'bx", "1'bx"), + ("(1'bx == 1'bx) ? 1'b0 : 1'b1", "1'bx"), + ("(1'bx == 1'bx) ? 1'bz : 1'bz", "1'bz"), + ("1'sbx == 2'b1x", "1'b0"), + ("1'sbx != 2'sb1x", "1'bx"), + ("8'hff == '1", "1'b1"), + ] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({selector}) {expected}: y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("equality_constant_case.sv")) + .unwrap_or_else(|error| panic!("{selector}: {error}")); + } + + for op in ["==", "!="] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case (1'bx {op} 1'bx) \ + 1'b0, 1'b1: y = a; endcase endmodule" + ); + let error = analyze_source(&source, Path::new("unmatched_equality_case.sv")) + .expect_err("two-state labels cannot cover an unknown equality result"); + assert!(error.to_string().contains("latch inference"), "{error}"); + } + } + + #[test] + fn preserves_four_state_relational_case_selector_masks() { + for op in ["<", "<=", ">", ">="] { + for (left, right) in [("1'bx", "1'b1"), ("2'b1z", "2'b00")] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({left} {op} {right}) \ + 1'bx: y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("relational_constant_case.sv")) + .unwrap_or_else(|error| panic!("{left} {op} {right}: {error}")); + } + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case (1'bx {op} 1'b1) \ + 1'b0, 1'b1: y = a; endcase endmodule" + ); + let error = analyze_source(&source, Path::new("unmatched_relational_case.sv")) + .expect_err("two-state labels cannot cover an unknown relational result"); + assert!(error.to_string().contains("latch inference"), "{error}"); + } + } + + #[test] + fn folds_compound_four_state_case_labels() { + for (selector, label) in [ + ("1'bx", "(1'bx | 1'b0)"), + ("1'bx", "(1'bz & 1'b1)"), + ("2'bxz", "{1'bx, 1'bz}"), + ("2'bxx", "{2{1'bx}}"), + ("1'bz", "(1'bx ? 1'bz : 1'bz)"), + ("2'bxx", "(1'b1 ? 1'sbx : 2'sb00)"), + ("1'bx", "(1'bx < 1'b1)"), + ("1'bx", "label()"), + ("4'b1111", "'1"), + ("4'sb1111", "'1"), + ] { + let source = format!( + "module Top(input logic a, output logic y); \ + function logic label(); return 1'bx | 1'b0; endfunction \ + always_comb case ({selector}) {label}: y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("compound_constant_case_label.sv")) + .unwrap_or_else(|error| panic!("{selector}, {label}: {error}")); + } + let error = analyze_source( + "module Top(input logic a, output logic y); \ + always_comb case (1'bz) (1'bx | 1'b0): y = a; endcase endmodule", + Path::new("unmatched_compound_case_label.sv"), + ) + .expect_err("an X-valued label must not cover a Z-valued selector"); + assert!(error.to_string().contains("latch inference"), "{error}"); + } + + #[test] + fn preserves_use_site_dimensions_in_parameter_alias_types() { + let source = r#" + module Top #(parameter W = 4, N = 2) (); + typedef logic [3:0] nibble_t; + typedef logic signed [3:0] signed_nibble_t; + parameter nibble_t [1:0] P = 8'hab; + localparam nibble_t [W'(2):W'(1)] L = P; + localparam signed_nibble_t [1:0] S = 8'hab; + localparam nibble_t [1:0] F = '1; + localparam logic [nibble_t'(7):nibble_t'(0)] B = 8'hab; + localparam BITS = $bits(P); + parameter nibble_t [N-1:0] R = 16'hcdef; + endmodule + "#; + for (overrides, p_value, r_width, r_value) in [ + (HashMap::default(), 0xab, 8, 0xef), + ( + [("P".to_string(), 0xcd), ("N".to_string(), 4)] + .into_iter() + .collect(), + 0xcd, + 16, + 0xcdef, + ), + ] { + let ir = analyze_source_with_module_parameter_overrides( + source, + Path::new("parameter_alias_use_site_dimensions.sv"), + "Top", + &overrides, + ) + .expect("parameter aliases should retain use-site packed dimensions"); + let parameters = ir.modules()[0].parameters(); + for (name, width, signed, value) in [ + ("P", Some(8), Some(false), p_value), + ("L", Some(8), Some(false), p_value), + ("S", Some(8), Some(true), -85), + ("F", Some(8), Some(false), 255), + ("B", Some(8), Some(false), 0xab), + ("BITS", None, None, 8), + ("R", Some(r_width), Some(false), r_value), + ] { + let parameter = parameters + .iter() + .find(|parameter| parameter.name() == name) + .unwrap(); + assert_eq!(parameter.declared_width(), width, "{name}"); + assert_eq!(parameter.declared_signed(), signed, "{name}"); + assert_eq!(parameter.resolved_value(), Some(value), "{name}"); + } + } + } + + #[test] + fn preserves_four_state_arithmetic_case_constants() { + for (expression, expected) in [ + ("1'bx + 1'b0", "1'bx"), + ("2'b1z - 4'b0001", "4'bxxxx"), + ("4'b0000 * 2'b1x", "4'bxxxx"), + ("2'b1z / 2'b01", "2'bxx"), + ("2'b1x % 2'b01", "2'bxx"), + ("2'b01 / 2'b1x", "2'bxx"), + ("2'b01 % 2'b1z", "2'bxx"), + ("+(2'b1z)", "2'bxx"), + ("-(2'b1z)", "2'bxx"), + ("(2'b11 + 2'b01) + 2'b0x", "2'bxx"), + ("(2'b1x + 2'b01) & 2'b00", "2'b00"), + ] { + for (selector, label) in [(expression, expected), (expected, expression)] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({selector}) ({label}): y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("arithmetic_case_constants.sv")) + .unwrap_or_else(|error| panic!("{selector}, {label}: {error}")); + } + } + let error = analyze_source( + "module Top(input logic a, output logic y); \ + always_comb case (1'bx + 1'b0) 1'b0, 1'b1, 1'bz: y = a; endcase endmodule", + Path::new("unmatched_arithmetic_case.sv"), + ) + .expect_err("arithmetic X results must not match known values or Z"); + assert!(error.to_string().contains("latch inference"), "{error}"); + } + + #[test] + fn preserves_four_state_reduction_case_constants() { + for (expression, expected) in [ + ("&1'bx", "1'bx"), + ("|1'bz", "1'bx"), + ("^2'b1x", "1'bx"), + ("&3'b1z0", "1'b0"), + ("|3'b0z1", "1'b1"), + ("~&2'b1z", "1'bx"), + ("~|2'b0x", "1'bx"), + ("~^2'b1z", "1'bx"), + ("^~2'b1x", "1'bx"), + ("^'1", "1'b1"), + ("&'z", "1'bx"), + ("&(4'b1x11 & 4'b0111)", "1'b0"), + ("|(4'b0z00 | 4'b1000)", "1'b1"), + ( + "^256'hffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "1'b0", + ), + ] { + for (selector, label) in [(expression, expected), (expected, expression)] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({selector}) ({label}): y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("reduction_case_constants.sv")) + .unwrap_or_else(|error| panic!("{selector}, {label}: {error}")); + } + } + let error = analyze_source( + "module Top(input logic a, output logic y); \ + always_comb case (&1'bx) 1'b0, 1'b1, 1'bz: y = a; endcase endmodule", + Path::new("unmatched_reduction_case.sv"), + ) + .expect_err("reduction X results must not match known values or Z"); + assert!(error.to_string().contains("latch inference"), "{error}"); + } + + #[test] + fn preserves_use_site_dimensions_in_function_alias_types() { + let ir = analyze_source( + r#" + module Top #(parameter W = 4)(output logic [7:0] y); + typedef logic [3:0] nibble_t; + function automatic nibble_t [W'(2):W'(1)] f(); + return 8'hab; + endfunction + localparam BITS = $bits(f())'(16'hffff); + localparam SIZE = $size(f())'(8'hff); + always_comb y = f(); + endmodule + "#, + Path::new("function_alias_use_site_dimensions.sv"), + ) + .expect("function aliases should retain their use-site packed dimensions"); + let parameters = ir.modules()[0].parameters(); + assert_eq!(parameters[1].resolved_value(), Some(255)); + assert_eq!(parameters[2].resolved_value(), Some(3)); + } + + #[test] + fn preserves_selected_size_argument_dimensions() { + for (argument, expected) in [ + ("a", 5), + ("a[0]", 3), + ("a[0][0]", 2), + ("a[0][0][0]", 4), + ("a[0][0][0][0]", 1), + ("(a[0][0])", 2), + ("a[0][0][1:0]", 2), + ("a[0][0][0][2:1]", 2), + ] { + let source = format!( + "module Top(output logic [31:0] y); \ + logic [1:0][3:0] a[5][3]; \ + localparam P = $size({argument})'(32'hffff_ffff); \ + always_comb y = P; endmodule" + ); + let ir = analyze_source(&source, Path::new("selected_size_dimensions.sv")) + .unwrap_or_else(|error| panic!("{argument}: {error}")); + assert_eq!( + ir.modules()[0].parameters()[0].resolved_value(), + Some((1 << expected) - 1), + "$size({argument})" + ); + } + } + + #[test] + fn resolves_alias_casts_in_generate_local_parameters() { + for declaration in ["localparam S = t'(4);", "localparam t S = 4;"] { + let source = format!( + r#" + module Top(output logic y); + typedef logic [1:0] t; + if (1) begin : selected + {declaration} + if (S) begin : disabled + function automatic logic invalid(input real x); + return x; + endfunction + end else begin : enabled + assign y = 1'b1; + end + end + endmodule + "# + ); + let ir = analyze_source(&source, Path::new("generate_local_alias_cast.sv")) + .expect("generate-local aliases must select the reachable branch"); + assert_eq!(ir.modules()[0].comb_processes().len(), 1); + } + } + + #[test] + fn preserves_known_conditional_case_selector_types() { + for (selector, label) in [ + ("1'b1 ? 1'sb1 : 2'sb00", "2'b11"), + ("1'b0 ? 2'sb00 : 1'sb1", "2'b11"), + ("1'b1 ? 1'sb1 : 2'b00", "2'b01"), + ("1'b1 ? 1'sbx : 2'sb00", "2'bxx"), + ("1'b1 ? 1'sbz : 2'b00", "2'b0z"), + ("1'b1 ? '1 : 2'b00", "2'b11"), + ] { + let source = format!( + "module Top(input logic a, output logic y); \ + always_comb case ({selector}) {label}: y = a; endcase endmodule" + ); + analyze_source(&source, Path::new("known_conditional_case.sv")) + .unwrap_or_else(|error| panic!("{selector}: {error}")); + } + } + + #[test] + fn recognizes_single_bit_two_state_bitwise_complements() { + for predicate in ["s", "~(~s)", "s != 0"] { + let source = format!( + "module Top(input bit s, input logic outer, a, b, output logic y); \ + always_comb if (outer) begin \ + if ({predicate}) y = a; if (~({predicate})) y = b; \ + end else y = a; endmodule" + ); + analyze_source(&source, Path::new("one_bit_complements.sv")) + .unwrap_or_else(|error| panic!("{predicate}: {error}")); + } + for declaration in ["logic s", "bit [1:0] s"] { + let source = format!( + "module Top(input {declaration}, input logic outer, a, b, output logic y); \ + always_comb if (outer) begin \ + if (s) y = a; if (~s) y = b; end else y = a; endmodule" + ); + assert!( + analyze_source(&source, Path::new("non_complementary_bitwise_guards.sv")) + .expect_err("only a one-bit two-state bitwise inverse proves coverage") + .to_string() + .contains("latch inference inside always_comb") + ); + } + } + + #[test] + fn resolves_size_casts_in_declaration_ranges_without_recursion() { + for declaration in [ + "output logic [$bits(f())'(7):0] y", + "output logic [$size(f())'(7):0] y", + ] { + let source = format!( + "module Top({declaration}); \ + function logic [7:0] f(); return '0; endfunction \ + logic [$bits(f())'(7):0] a; \ + always_comb begin a = '1; y = a; end endmodule" + ); + let ir = analyze_source(&source, Path::new("declaration_size_cast.sv")) + .expect("size casts must not rebuild the declaration recursively"); + assert_eq!( + ir.modules()[0].ports()[0].r#type().resolved_width(), + Some(8) + ); + assert_eq!( + ir.modules()[0].signals()[0].r#type().resolved_width(), + Some(8) + ); + } + } + + #[test] + fn applies_function_return_types_in_procedural_lvalue_indices() { + let ir = analyze_source( + r#" + module Top(input bit [1:0] index, input logic data, output logic [1:0] x); + function automatic bit idx(); + return index; + endfunction + always_comb begin + x = '0; + x[idx()] = data; + end + endmodule + "#, + Path::new("function_typed_lvalue_index.sv"), + ) + .expect("the one-bit function return should truncate the expanded lvalue index"); + assert!( + ir.modules()[0].comb_processes()[0] + .assignments() + .iter() + .any(|assignment| assignment.lhs() == "x" + && expr_references_ident_name(assignment.rhs(), "data")), + "the selected write must not be dropped: {:?}", + ir.modules()[0].comb_processes()[0].assignments() + ); + } + + #[test] + fn expands_function_calls_in_case_labels_for_coverage() { + analyze_source( + r#" + module Top(input logic outer, a, b, output logic y); + function automatic bit zero(); + return 1'b0; + endfunction + always_comb begin + if (outer) begin + case (1'b0) + zero(): y = a; + endcase + end else begin + y = b; + end + end + endmodule + "#, + Path::new("function_case_label_coverage.sv"), + ) + .expect("a folded function case label should make the branch exhaustive"); + } + + #[test] + fn skips_inactive_generate_blocks_with_parameter_casts() { + for condition in ["W'(4)", "select_t'(4)"] { + let source = format!( + r#" + module Top #(parameter W = 2)(output logic y); + typedef logic [W-1:0] select_t; + if ({condition}) begin : disabled + function automatic logic invalid(input real x); + return x; + endfunction + end else begin : enabled + assign y = 1'b1; + end + endmodule + "# + ); + let ir = analyze_source(&source, Path::new("inactive_generate_cast.sv")) + .expect("cast truncation should select the supported generate branch"); + assert_eq!(ir.modules()[0].comb_processes().len(), 1); + } + } + + #[test] + fn skips_inactive_loop_generate_blocks_with_parameter_casts() { + let source = r#" + module Top #(parameter W = 2)(output logic y); + typedef logic [W-1:0] select_t; + for (genvar i = W'(0); i < select_t'(4); i += W'(1)) begin : disabled + function automatic logic invalid(input real x); + return x; + endfunction + end + assign y = 1'b1; + endmodule + "#; + let ir = analyze_source(source, Path::new("inactive_loop_generate_cast.sv")) + .expect("a zero-iteration loop must skip unsupported declarations"); + assert_eq!(ir.modules()[0].comb_processes().len(), 1); + } + + #[test] + fn caps_aggregate_nested_static_loop_expansion() { + let error = analyze_source( + r#" + module Top(output logic y); + always_comb begin + y = 1'b0; + for (int i = 0; i < 101; i++) + for (int j = 0; j < 100; j++) + y = 1'b1; + end + endmodule + "#, + Path::new("nested_static_loop_budget.sv"), + ) + .expect_err("nested loop expansion must have an aggregate bound") + .to_string(); + assert!(error.contains("procedural loop unroll limit exceeded")); + } + + #[test] + fn restricts_enum_alias_types_to_the_declared_base() { + let ir = analyze_source( + r#" + module Top(output E y); + typedef enum logic { A = int'(1) } E; + always_comb y = A; + endmodule + "#, + Path::new("enum_member_cast_type_is_not_base.sv"), + ) + .expect("types in enum member initializers must not replace the enum base"); + assert_eq!( + ir.modules()[0].ports()[0].r#type().resolved_width(), + Some(1) + ); + assert!(!ir.modules()[0].ports()[0].r#type().is_signed()); + } + + #[test] + fn recognizes_complete_four_state_cases() { + analyze_source( + r#" + module Top(input logic s, input logic a, output logic y); + always_comb begin + case (s) + 1'b0: y = a; + 1'b1: y = a; + 1'bx: y = a; + 1'bz: y = a; + endcase + end + endmodule + "#, + Path::new("complete_four_state_case.sv"), + ) + .expect("all four states should exhaust a one-bit logic selector"); + } + + #[test] + fn recognizes_exhaustive_constant_four_state_cases() { + analyze_source( + r#" + module Top(input logic c, a, b, output logic y); + always_comb begin + case (1'bx) + 1'bx: if (c) y = a; else y = b; + endcase + end + endmodule + "#, + Path::new("constant_four_state_case_selector.sv"), + ) + .expect("a matching X-valued constant case item should be exhaustive"); + } + + #[test] + fn expands_constant_function_case_selectors_for_coverage() { + analyze_source( + r#" + module Top(input logic a, output logic y); + function automatic bit f(); + return 1'b0; + endfunction + always_comb begin + case (f()) + 1'b0: y = a; + endcase + end + endmodule + "#, + Path::new("constant_function_case_selector.sv"), + ) + .expect("a constant function selector should make its matching item exhaustive"); + + analyze_source( + r#" + module Top(input logic a, output logic y); + function automatic logic f(); + return 1'bx; + endfunction + always_comb begin + case (f()) + 1'bx: y = a; + endcase + end + endmodule + "#, + Path::new("constant_unknown_function_case_selector.sv"), + ) + .expect("an X-valued constant function selector should preserve its mask"); + } + + #[test] + fn expands_function_calls_in_procedural_lvalue_indices() { + analyze_source( + r#" + module Top(input bit index, input logic data, output logic [1:0] x); + function automatic bit idx(); + return index; + endfunction + always_comb begin + x = '0; + x[idx()] = data; + end + endmodule + "#, + Path::new("function_lvalue_index.sv"), + ) + .expect("a supported function call in an lvalue index should be expanded"); + } + + #[test] + fn resolves_value_dependent_type_parameter_defaults() { + let ir = analyze_source( + r#" + module Top #( + parameter W = 8, + parameter type T = logic [W'(7):0] + ) (output T y); + always_comb y = 8'hff; + endmodule + "#, + Path::new("value_dependent_type_parameter.sv"), + ) + .expect("a type parameter default should use preceding value parameters"); + assert_eq!( + ir.modules()[0].ports()[0].r#type().resolved_width(), + Some(8) + ); + } + + #[test] + fn caps_dynamic_select_normalization_expansion() { + let error = analyze_source( + r#" + module Top( + input logic [16:0] index, + input logic data, replace, + output logic [4096:0] value + ); + always_comb begin + value = '0; + value[index] = data; + if (replace) value = '1; + end + endmodule + "#, + Path::new("capped_dynamic_select_expansion.sv"), + ) + .expect_err("oversized dynamic-select expansion should be rejected compactly"); + assert!( + error + .to_string() + .contains("dynamic selected write expansion exceeds limit") + ); + } + + #[test] + fn retains_zero_iteration_loop_writes_for_latch_detection() { + let error = analyze_source( + r#" + module Top(input logic a, output logic y); + always_comb + for (int i = 0; i < 0; i++) y = a; + endmodule + "#, + Path::new("zero_iteration_comb_loop.sv"), + ) + .expect_err("a zero-iteration loop must not silently discard its target"); + assert!( + error + .to_string() + .contains("latch inference inside always_comb") + ); + + let error = analyze_source( + r#" + module Top(input logic enable, a, output logic y); + always_comb begin + if (enable) + for (int i = 0; i < 0; i++) y = a; + end + endmodule + "#, + Path::new("nested_zero_iteration_comb_loop.sv"), + ) + .expect_err("a nested zero-iteration loop must retain its write target"); + assert!( + error + .to_string() + .contains("latch inference inside always_comb") + ); + + analyze_source( + r#" + module Top(input logic a, output logic y); + always_comb begin + y = 1'b0; + for (int i = 0; i < 0; i++) y = a; + end + endmodule + "#, + Path::new("initialized_zero_iteration_comb_loop.sv"), + ) + .expect("a preceding assignment should initialize a zero-iteration loop target"); + } + + #[test] + fn materializes_static_loop_indices_in_definite_write_targets() { + analyze_source( + r#" + module Top(input logic c, a, b, output logic [1:0] x); + always_comb begin + if (c) begin + x[0] = a; + x[1] = a; + end else begin + for (int i = 0; i < 2; i++) x[i] = b; + end + end + endmodule + "#, + Path::new("materialized_definite_loop_targets.sv"), + ) + .expect("each static loop iteration should contribute its concrete target"); + } + + #[test] + fn preserves_named_constant_casts_in_packed_ranges() { + let ir = analyze_source( + r#" + module Top #( + parameter W = 8 + ) ( + output logic [W'(15):0] y + ); + endmodule + "#, + Path::new("named_cast_packed_range.sv"), + ) + .expect("named constant casts should lower with the module environment"); + assert_eq!( + ir.modules()[0].ports()[0].r#type().resolved_width(), + Some(16) + ); + } + + #[test] + fn resolves_named_casts_while_collecting_parameter_ranges() { + let ir = analyze_source( + r#" + module Top #( + parameter W = 3, + parameter logic [W'(2):0] P = 3'b100 + ) (); + endmodule + "#, + Path::new("named_cast_parameter_range.sv"), + ) + .expect("parameter ranges should use preceding parameters in named casts"); + let parameter = &ir.modules()[0].parameters()[1]; + assert_eq!(parameter.declared_width(), Some(3)); + assert_eq!(parameter.resolved_value(), Some(4)); + } + + #[test] + fn preserves_named_constant_casts_in_unpacked_ranges() { + let ir = analyze_source( + r#" + module Top #( + parameter W = 2 + ) ( + output logic y [W'(1):0] + ); + endmodule + "#, + Path::new("named_cast_unpacked_range.sv"), + ) + .expect("named constant casts in unpacked dimensions should use the module environment"); + assert_eq!( + ir.modules()[0].ports()[0].r#type().resolved_width(), + Some(2) + ); + } + + #[test] + fn preserves_operand_signedness_for_named_numeric_size_casts() { + let ir = analyze_source( + r#" + module Top; + localparam W = 8; + localparam P = W'(4'shf); + localparam Q = 16'(P); + endmodule + "#, + Path::new("named_numeric_size_cast.sv"), + ) + .expect("named numeric size casts should preserve operand signedness"); + let parameters = ir.modules()[0].parameters(); + assert_eq!(parameters[1].resolved_value(), Some(-1)); + assert_eq!(parameters[1].resolved_signed(), Some(true)); + assert_eq!(parameters[2].resolved_value(), Some(-1)); + } + + #[test] + fn applies_typedef_signedness_to_constant_primary_cast_targets() { + let ir = analyze_source( + r#" + module Top; + typedef logic signed [7:0] S; + localparam P = S'(8'hff); + endmodule + "#, + Path::new("constant_primary_typedef_cast.sv"), + ) + .expect("a typedef cast should use the typedef signedness"); + let parameter = &ir.modules()[0].parameters()[0]; + assert_eq!(parameter.resolved_value(), Some(-1)); + assert_eq!(parameter.resolved_signed(), Some(true)); + } + + #[test] + fn preserves_casted_ranges_while_collecting_typedefs() { + let ir = analyze_source( + r#" + module Top #(parameter W = 8); + typedef logic [W'(15):0] T; + T x; + endmodule + "#, + Path::new("casted_typedef_range.sv"), + ) + .expect("typedef ranges should use the populated module environment"); + assert_eq!( + ir.modules()[0].signals()[0].r#type().resolved_width(), + Some(16) + ); + } + + #[test] + fn preserves_named_casts_in_constant_select_indices() { + let ir = analyze_source( + r#" + module Top; + localparam W = 1; + localparam logic [1:0] A = 2'b10; + localparam B = A[W'(0)]; + endmodule + "#, + Path::new("casted_constant_select.sv"), + ) + .expect("constant select indices should use the populated module environment"); + assert_eq!(ir.modules()[0].parameters()[2].resolved_value(), Some(0)); + } + + #[test] + fn converts_unknowns_when_constant_casting_to_two_state_types() { + let ir = analyze_source( + r#" + module Top; + typedef bit [1:0] two_t; + localparam logic [1:0] P = two_t'(2'bx1); + endmodule + "#, + Path::new("two_state_constant_cast.sv"), + ) + .expect("two-state constant casts should convert unknown bits to zero"); + assert_eq!(ir.modules()[0].parameters()[0].resolved_value(), Some(1)); + } + + #[test] + fn resolves_typedef_declared_parameter_types() { + let ir = analyze_source( + r#" + module Top; + typedef enum logic signed [1:0] { Z = 0 } E; + localparam E P = '0; + localparam B = $bits(P); + endmodule + "#, + Path::new("typedef_declared_parameter.sv"), + ) + .expect("typedef-declared parameters should retain their declared type"); + let parameters = ir.modules()[0].parameters(); + assert_eq!(parameters[0].declared_width(), Some(2)); + assert_eq!(parameters[0].declared_signed(), Some(true)); + assert_eq!(parameters[1].resolved_value(), Some(2)); + } + + #[test] + fn preserves_enum_types_in_instance_parameter_overrides() { + let ir = analyze_source( + r#" + module Child #(parameter P = 0) (); + endmodule + + module Top; + typedef enum logic signed [1:0] { A = 2'b10 } E; + Child #(.P(A)) child(); + endmodule + "#, + Path::new("typed_enum_parameter_override.sv"), + ) + .expect("enum parameter overrides should retain their declared type"); + let top = ir + .modules() + .iter() + .find(|module| module.name() == "Top") + .expect("Top module should exist"); + assert_eq!( + top.instances()[0].parameter_overrides()[0].value(), + Some(&ir::ConstExpr::Literal("2'sd2".to_string())) + ); + } + + #[test] + fn rejects_conditional_predicate_conjunction_terms() { + let error = analyze_source( + r#" + module Top(input logic a, b, output logic y); + always_comb begin + if (a &&& b) y = 1'b1; + else y = 1'b0; + end + endmodule + "#, + Path::new("predicate_conjunction.sv"), + ) + .expect_err("unsupported predicate conjunctions must not be partially lowered") + .to_string(); + assert!( + error.contains("predicate lowering"), + "unexpected error: {error}" + ); + } + #[test] fn analyzes_basic_sv_module_name() { let ir = analyze_source( @@ -486,7 +2822,7 @@ mod tests { .iter() .find(|parameter| parameter.name() == "MAX_COUNT") .and_then(|parameter| parameter.resolved_value()), - Some(0xffff_ffff) + Some(3) ); assert_eq!( counter diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index 758518c1f..bd54bacc1 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -53,13 +53,13 @@ pub fn eval_const_expr(expr: &ConstExpr, constants: &HashMap) -> O value.checked_shr(bit).map(|value| value & 1) } ConstExpr::Function { name, args } => eval_const_function(name, args, constants), - ConstExpr::Unary { op, expr } => { - if let ConstExpr::Literal(literal) = &**expr - && let Some(result) = eval_literal_unary(*op, literal) + ConstExpr::Unary { op, expr: operand } => { + if let Some(result) = integral_literal_from_const_expr(expr) + && let Some(value) = integral_literal_as_i128(&result, result.signed) { - return Some(result); + return Some(value); } - let value = eval_const_expr(expr, constants)?; + let value = eval_const_expr(operand, constants)?; match op { UnaryOp::Plus => Some(value), UnaryOp::Minus => value.checked_neg(), @@ -146,6 +146,53 @@ pub fn eval_const_expr_with_types( eval_const_expr(&expr, constants) } +/// Evaluate a literal constant expression without discarding four-state masks. +pub fn eval_const_integral_literal_with_types( + expr: &ConstExpr, + constants: &HashMap, + types: &HashMap, +) -> Option { + let expr = substitute_typed_constants(expr.clone(), constants, types); + integral_literal_from_const_expr(&expr) +} + +/// Evaluate and resize a constant expression in a case selector's comparison +/// context without discarding X/Z masks. +pub fn context_size_const_integral_literal( + expr: &ConstExpr, + constants: &HashMap, + types: &HashMap, + width: usize, + signed: bool, +) -> Option { + if let Some(fill) = unbased_fill_from_const_expr(expr) { + let mut literal = integral_fill_literal(fill, width)?; + literal.signed = signed; + return Some(literal); + } + let literal = eval_const_integral_literal_with_types(expr, constants, types)?; + let extension = signed_extension(&literal, literal.signed); + Some(resize_integral_literal(literal, width, signed, extension)) +} + +pub fn format_integral_literal_binary(literal: &IntegralLiteral) -> String { + let bits = (0..literal.width) + .rev() + .map(|bit| { + let bit = bit as u64; + if literal.mask.bit(bit) { + if literal.value.bit(bit) { 'x' } else { 'z' } + } else if literal.value.bit(bit) { + '1' + } else { + '0' + } + }) + .collect::(); + let signing = if literal.signed { "s" } else { "" }; + format!("{}'{signing}b{bits}", literal.width) +} + pub fn substitute_typed_constants( expr: ConstExpr, constants: &HashMap, @@ -209,10 +256,13 @@ fn format_typed_constant_literal(value: i128, width: usize, signed: bool) -> Str } } -fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Option { +fn integral_binary_operands( + left: &ConstExpr, + right: &ConstExpr, +) -> Option<(IntegralLiteral, IntegralLiteral)> { let left_fill = unbased_fill_from_const_expr(left); let right_fill = unbased_fill_from_const_expr(right); - let (mut left, mut right) = match (left_fill, right_fill) { + Some(match (left_fill, right_fill) { (Some(left_fill), Some(right_fill)) => ( integral_fill_literal(left_fill, 1)?, integral_fill_literal(right_fill, 1)?, @@ -230,7 +280,11 @@ fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Opt integral_literal_from_const_expr(left)?, integral_literal_from_const_expr(right)?, ), - }; + }) +} + +fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Option { + let (mut left, mut right) = integral_binary_operands(left, right)?; if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar) { if left.mask != BigUint::default() || right.mask != BigUint::default() { return None; @@ -295,109 +349,123 @@ fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Opt signed, signed_extension(&right, signed), ); - if matches!(op, BinaryOp::LogicAnd | BinaryOp::LogicOr) - || left.mask != BigUint::default() - || right.mask != BigUint::default() - { - if let Some(result) = eval_four_state_binary(&left, op, &right, signed) { - return Some(result); - } - if left.mask != BigUint::default() || right.mask != BigUint::default() { - return None; - } - } + eval_four_state_binary(&left, op, &right, signed) +} + +fn eval_four_state_binary( + left: &IntegralLiteral, + op: BinaryOp, + right: &IntegralLiteral, + signed: bool, +) -> Option { + let result = eval_four_state_binary_literal(left, op, right, signed)?; + integral_literal_as_i128(&result, result.signed) +} + +fn eval_four_state_binary_literal( + left: &IntegralLiteral, + op: BinaryOp, + right: &IntegralLiteral, + signed: bool, +) -> Option { if matches!( op, - BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod ) { - let ordering = if signed { - integral_literal_as_i128(&left, true)?.cmp(&integral_literal_as_i128(&right, true)?) - } else { - left.value.cmp(&right.value) - }; - let result = match op { - BinaryOp::Eq => ordering.is_eq(), - BinaryOp::Ne => ordering.is_ne(), - BinaryOp::Lt => ordering.is_lt(), - BinaryOp::Le => ordering.is_le(), - BinaryOp::Gt => ordering.is_gt(), - BinaryOp::Ge => ordering.is_ge(), - _ => unreachable!(), - }; - return Some(result as i128); - } - let modulus = BigUint::from(1u8) << width; - let width_mask = &modulus - BigUint::from(1u8); - if matches!(op, BinaryOp::Div | BinaryOp::Mod) { - if right.value == BigUint::default() { - return None; - } - if signed { - let left = integral_literal_as_i128(&left, true)?; - let right = integral_literal_as_i128(&right, true)?; - let value = match op { - BinaryOp::Div if left == i128::MIN && right == -1 => i128::MIN, - BinaryOp::Mod if left == i128::MIN && right == -1 => 0, - BinaryOp::Div => left.checked_div(right)?, - BinaryOp::Mod => left.checked_rem(right)?, - _ => unreachable!(), - }; - let value = BigUint::from(value as u128) & &width_mask; - return integral_literal_as_i128( - &IntegralLiteral { - width, - signed, - value, - mask: BigUint::default(), - }, - true, - ); + let width = left.width; + let modulus = BigUint::from(1u8) << width; + let width_mask = &modulus - BigUint::from(1u8); + // Arithmetic propagates any X/Z bit to the entire result, retaining + // the common operand width and signedness. Division by zero does too. + if left.mask != BigUint::default() + || right.mask != BigUint::default() + || (matches!(op, BinaryOp::Div | BinaryOp::Mod) && right.value == BigUint::default()) + { + return Some(IntegralLiteral { + width, + signed, + value: width_mask.clone(), + mask: width_mask, + }); } let value = match op { - BinaryOp::Div => left.value / right.value, - BinaryOp::Mod => left.value % right.value, + BinaryOp::Add => (&left.value + &right.value) & &width_mask, + BinaryOp::Sub => (&left.value + &modulus - &right.value) & &width_mask, + BinaryOp::Mul => (&left.value * &right.value) & &width_mask, + BinaryOp::Div | BinaryOp::Mod => { + let negative = |literal: &IntegralLiteral| { + signed && width != 0 && literal.value.bit((width - 1) as u64) + }; + let magnitude = |literal: &IntegralLiteral| { + if negative(literal) { + &modulus - &literal.value + } else { + literal.value.clone() + } + }; + let (value, negative) = if op == BinaryOp::Div { + ( + magnitude(left) / magnitude(right), + negative(left) ^ negative(right), + ) + } else { + (magnitude(left) % magnitude(right), negative(left)) + }; + if negative && value != BigUint::default() { + &modulus - value + } else { + value + } + } _ => unreachable!(), }; - return i128::try_from(value).ok(); - } - let value = match op { - BinaryOp::Add => (left.value + right.value) & &width_mask, - BinaryOp::Sub => (left.value + &modulus - right.value) & &width_mask, - BinaryOp::Mul => (left.value * right.value) & &width_mask, - BinaryOp::BitAnd => left.value & right.value, - BinaryOp::BitOr => left.value | right.value, - BinaryOp::BitXor => left.value ^ right.value, - _ => unreachable!(), - }; - integral_literal_as_i128( - &IntegralLiteral { + return Some(IntegralLiteral { width, signed, value, mask: BigUint::default(), - }, - signed, - ) -} - -fn eval_four_state_binary( - left: &IntegralLiteral, - op: BinaryOp, - right: &IntegralLiteral, - signed: bool, -) -> Option { + }); + } if matches!(op, BinaryOp::LogicAnd | BinaryOp::LogicOr) { let left = integral_literal_truth(left); let right = integral_literal_truth(right); - return match op { - BinaryOp::LogicAnd if left == Some(false) || right == Some(false) => Some(0), - BinaryOp::LogicAnd if left == Some(true) && right == Some(true) => Some(1), - BinaryOp::LogicOr if left == Some(true) || right == Some(true) => Some(1), - BinaryOp::LogicOr if left == Some(false) && right == Some(false) => Some(0), + let truth = match op { + BinaryOp::LogicAnd if left == Some(false) || right == Some(false) => Some(false), + BinaryOp::LogicAnd if left == Some(true) && right == Some(true) => Some(true), + BinaryOp::LogicOr if left == Some(true) || right == Some(true) => Some(true), + BinaryOp::LogicOr if left == Some(false) && right == Some(false) => Some(false), _ => None, }; + return Some(integral_literal_from_truth(truth)); } - if !matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) { + if matches!( + op, + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge + ) { + // Relational comparisons are unknown if either operand contains X/Z, + // even when known bits alone would establish an ordering. + if left.mask != BigUint::default() || right.mask != BigUint::default() { + return Some(integral_literal_from_truth(None)); + } + let negative = |literal: &IntegralLiteral| { + signed && literal.width != 0 && literal.value.bit((literal.width - 1) as u64) + }; + let ordering = negative(right) + .cmp(&negative(left)) + .then_with(|| left.value.cmp(&right.value)); + let truth = match op { + BinaryOp::Lt => ordering.is_lt(), + BinaryOp::Le => ordering.is_le(), + BinaryOp::Gt => ordering.is_gt(), + BinaryOp::Ge => ordering.is_ge(), + _ => unreachable!(), + }; + return Some(integral_literal_from_truth(Some(truth))); + } + if !matches!( + op, + BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Eq | BinaryOp::Ne + ) { return None; } @@ -405,6 +473,21 @@ fn eval_four_state_binary( let width_mask = (BigUint::from(1u8) << width) - BigUint::from(1u8); let left_known = &width_mask ^ &left.mask; let right_known = &width_mask ^ &right.mask; + if matches!(op, BinaryOp::Eq | BinaryOp::Ne) { + // A definite mismatch decides equality even if other bits are X/Z. + // Otherwise any unknown bit leaves a one-bit X result. + let mismatch = (&left.value ^ &right.value) & (&left_known & &right_known); + let equal = if mismatch != BigUint::default() { + Some(false) + } else if left.mask != BigUint::default() || right.mask != BigUint::default() { + None + } else { + Some(true) + }; + return Some(integral_literal_from_truth( + equal.map(|equal| equal ^ (op == BinaryOp::Ne)), + )); + } let left_one = &left.value & &left_known; let right_one = &right.value & &right_known; let left_zero = &left_known ^ &left_one; @@ -421,15 +504,21 @@ fn eval_four_state_binary( }; let known = &known_zero | &known_one; let mask = &width_mask ^ known; - integral_literal_as_i128( - &IntegralLiteral { - width, - signed, - value: known_one | &mask, - mask, - }, + Some(IntegralLiteral { + width, signed, - ) + value: known_one | &mask, + mask, + }) +} + +fn integral_literal_from_truth(truth: Option) -> IntegralLiteral { + IntegralLiteral { + width: 1, + signed: false, + value: BigUint::from(truth.unwrap_or(true) as u8), + mask: BigUint::from(truth.is_none() as u8), + } } fn integral_literal_truth(literal: &IntegralLiteral) -> Option { @@ -464,8 +553,16 @@ fn merge_unknown_const_mux_arms( return Some(then_value); } - let mut then_literal = integral_literal_from_const_expr(then_expr)?; - let mut else_literal = integral_literal_from_const_expr(else_expr)?; + let then_literal = integral_literal_from_const_expr(then_expr)?; + let else_literal = integral_literal_from_const_expr(else_expr)?; + let merged = merge_unknown_integral_literals(then_literal, else_literal); + integral_literal_as_i128(&merged, merged.signed) +} + +fn merge_unknown_integral_literals( + mut then_literal: IntegralLiteral, + mut else_literal: IntegralLiteral, +) -> IntegralLiteral { let width = then_literal.width.max(else_literal.width); let signed = then_literal.signed && else_literal.signed; let then_extension = signed_extension(&then_literal, signed); @@ -474,36 +571,103 @@ fn merge_unknown_const_mux_arms( else_literal = resize_integral_literal(else_literal, width, signed, else_extension); let width_mask = (BigUint::from(1u8) << width) - BigUint::from(1u8); - let same_value = &width_mask ^ (&then_literal.value ^ &else_literal.value); - let same_mask = &width_mask ^ (&then_literal.mask ^ &else_literal.mask); - let matching = same_value & same_mask; - let mask = &then_literal.mask | &else_literal.mask | (&width_mask ^ &matching); - let value = (&then_literal.value & &matching) | &mask; - integral_literal_as_i128( - &IntegralLiteral { - width, - signed, - value, - mask, - }, + let different = ((&then_literal.value ^ &else_literal.value) + | (&then_literal.mask ^ &else_literal.mask)) + & &width_mask; + let matching = &width_mask ^ &different; + IntegralLiteral { + width, signed, - ) + value: (&then_literal.value & &matching) | &different, + mask: (&then_literal.mask & matching) | different, + } } fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option { match expr { ConstExpr::Literal(literal) => parse_integral_literal(literal), - ConstExpr::Unary { - op: UnaryOp::BitNot, - expr, + ConstExpr::Unary { op, expr } => { + let operand = if matches!(op, UnaryOp::RedAnd | UnaryOp::RedOr | UnaryOp::RedXor) + && let Some(fill) = unbased_fill_from_const_expr(expr) + { + // Reduction operands are self-determined, so an unbased + // unsized fill contributes one bit (not an integer's width). + integral_fill_literal(fill, 1)? + } else { + integral_literal_from_const_expr(expr)? + }; + Some(eval_integral_unary(*op, operand)) + } + ConstExpr::Binary { left, op, right } + if matches!(op, BinaryOp::LogicAnd | BinaryOp::LogicOr) => + { + // Logical operands are self-determined; only the truth result + // has width one, including an unknown result. + let left = integral_literal_from_const_expr(left)?; + let right = integral_literal_from_const_expr(right)?; + eval_four_state_binary_literal(&left, *op, &right, false) + } + ConstExpr::Binary { left, op, right } + if matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + ) => + { + let (mut left, mut right) = integral_binary_operands(left, right)?; + let width = left.width.max(right.width); + let signed = left.signed && right.signed; + let left_extension = signed_extension(&left, signed); + let right_extension = signed_extension(&right, signed); + left = resize_integral_literal(left, width, signed, left_extension); + right = resize_integral_literal(right, width, signed, right_extension); + eval_four_state_binary_literal(&left, *op, &right, signed) + } + ConstExpr::Mux { + condition, + then_expr, + else_expr, } => { - let ConstExpr::Literal(literal) = &**expr else { - return None; + let then_literal = integral_literal_from_const_expr(then_expr)?; + let else_literal = integral_literal_from_const_expr(else_expr)?; + let arm_width = |expr, width| { + if unbased_fill_from_const_expr(expr).is_some() { + 1 + } else { + width + } + }; + let width = arm_width(then_expr, then_literal.width) + .max(arm_width(else_expr, else_literal.width)); + let signed = then_literal.signed && else_literal.signed; + let resize_arm = |expr: &ConstExpr, literal: IntegralLiteral| { + if let Some(fill) = unbased_fill_from_const_expr(expr) { + let mut literal = integral_fill_literal(fill, width)?; + literal.signed = signed; + return Some(literal); + } + let extension = signed_extension(&literal, signed); + Some(resize_integral_literal(literal, width, signed, extension)) }; - let mut literal = parse_integral_literal(literal)?; - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - literal.value = width_mask ^ literal.value; - Some(literal) + let then_literal = resize_arm(then_expr, then_literal)?; + let else_literal = resize_arm(else_expr, else_literal)?; + match integral_literal_truth(&integral_literal_from_const_expr(condition)?) { + Some(true) => Some(then_literal), + Some(false) => Some(else_literal), + None => Some(merge_unknown_integral_literals(then_literal, else_literal)), + } } _ => None, } @@ -526,60 +690,59 @@ fn integral_literal_as_i128(literal: &IntegralLiteral, signed: bool) -> Option Option { - let literal_text = literal; - let literal = parse_integral_literal(literal_text)?; - if op == UnaryOp::ToTwoState { - let mut literal = literal; - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - literal.value &= width_mask ^ &literal.mask; - literal.mask = BigUint::default(); - return integral_literal_as_i128(&literal, literal.signed); - } - if literal.mask != BigUint::default() { - return None; - } - let value = literal_as_i128(literal_text)?; +fn eval_integral_unary(op: UnaryOp, mut literal: IntegralLiteral) -> IntegralLiteral { + let modulus = BigUint::from(1u8) << literal.width; + let width_mask = &modulus - BigUint::from(1u8); match op { - UnaryOp::Plus => Some(value), + UnaryOp::Plus | UnaryOp::Minus if literal.mask != BigUint::default() => { + literal.value = width_mask.clone(); + literal.mask = width_mask; + } + UnaryOp::Plus => {} UnaryOp::Minus => { - let modulus = BigUint::from(1u8) << literal.width; - let negated = if literal.value == BigUint::default() { - BigUint::default() - } else { - &modulus - literal.value - }; - integral_literal_as_i128( - &IntegralLiteral { - width: literal.width, - signed: literal.signed, - value: negated, - mask: BigUint::default(), - }, - literal.signed, - ) + if literal.value != BigUint::default() { + literal.value = modulus - literal.value; + } } - UnaryOp::BitNot if literal.signed => Some(!value), UnaryOp::BitNot => { - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - i128::try_from(width_mask ^ literal.value).ok() + let known = &width_mask ^ &literal.mask; + literal.value = ((&width_mask ^ literal.value) & known) | &literal.mask; + } + UnaryOp::LogicNot => { + return integral_literal_from_truth( + integral_literal_truth(&literal).map(|truth| !truth), + ); + } + UnaryOp::ToTwoState => { + literal.value &= width_mask ^ &literal.mask; + literal.mask = BigUint::default(); } - UnaryOp::LogicNot => Some((literal.value == BigUint::default()) as i128), - UnaryOp::ToTwoState => Some(value), UnaryOp::RedAnd => { - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - Some((literal.value == width_mask) as i128) + // A known zero dominates any X/Z bits; otherwise all bits must + // be known ones for the reduction to be true. + let truth = if (&literal.value | &literal.mask) != width_mask { + Some(false) + } else if literal.mask == BigUint::default() { + Some(true) + } else { + None + }; + return integral_literal_from_truth(truth); + } + UnaryOp::RedOr => return integral_literal_from_truth(integral_literal_truth(&literal)), + UnaryOp::RedXor => { + let truth = (literal.mask == BigUint::default()).then(|| { + literal + .value + .iter_u64_digits() + .fold(false, |parity, digit| { + parity ^ (digit.count_ones() % 2 != 0) + }) + }); + return integral_literal_from_truth(truth); } - UnaryOp::RedOr => Some((literal.value != BigUint::default()) as i128), - UnaryOp::RedXor => Some( - (literal - .value - .iter_u64_digits() - .map(u64::count_ones) - .sum::() - & 1) as i128, - ), } + literal } fn eval_literal_four_state_equality( @@ -962,6 +1125,184 @@ fn extension_for_leading_digit(ch: char) -> (bool, bool) { mod literal_tests { use super::*; + #[test] + fn preserves_four_state_arithmetic_results() { + for op in [ + BinaryOp::Add, + BinaryOp::Sub, + BinaryOp::Mul, + BinaryOp::Div, + BinaryOp::Mod, + ] { + for (left, right, expected) in [ + ("1'bx", "1'b0", "1'bx"), + ("2'b1z", "4'b0001", "4'bxxxx"), + ("4'sb0001", "2'sb1z", "4'sbxxxx"), + ("4'sb0001", "2'b1z", "4'bxxxx"), + ("'z", "8'h01", "8'bxxxxxxxx"), + ("'x", "'1", "1'bx"), + ("256'hx", "1'b1", &format!("256'b{}", "x".repeat(256))), + ] { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal(left.to_string())), + op, + right: Box::new(ConstExpr::Literal(right.to_string())), + }; + let result = integral_literal_from_const_expr(&expr) + .unwrap_or_else(|| panic!("{left} {op:?} {right}")); + assert_eq!( + format_integral_literal_binary(&result), + expected, + "{left} {op:?} {right}" + ); + assert_eq!(eval_const_expr(&expr, &HashMap::default()), None); + } + } + for op in [UnaryOp::Plus, UnaryOp::Minus] { + for (operand, expected) in [("2'b1z", "2'bxx"), ("2'sb0x", "2'sbxx")] { + let expr = ConstExpr::Unary { + op, + expr: Box::new(ConstExpr::Literal(operand.to_string())), + }; + let result = integral_literal_from_const_expr(&expr).unwrap(); + assert_eq!(format_integral_literal_binary(&result), expected); + } + } + } + + #[test] + fn folds_nested_sized_arithmetic_without_losing_width_or_sign() { + for (left, op, right, expected) in [ + ("4'hf", BinaryOp::Add, "4'h1", "4'b0000"), + ("4'h0", BinaryOp::Sub, "4'h1", "4'b1111"), + ("4'h8", BinaryOp::Mul, "4'h2", "4'b0000"), + ("4'sh9", BinaryOp::Div, "4'sh2", "4'sb1101"), + ("4'sh9", BinaryOp::Mod, "4'sh2", "4'sb1111"), + ("4'sh9", BinaryOp::Div, "4'h2", "4'b0100"), + ("4'sh7", BinaryOp::Div, "4'she", "4'sb1101"), + ("4'sh7", BinaryOp::Mod, "4'she", "4'sb0001"), + ("4'sh8", BinaryOp::Div, "4'shf", "4'sb1000"), + ("4'sh8", BinaryOp::Mod, "4'shf", "4'sb0000"), + ("4'h7", BinaryOp::Div, "4'h0", "4'bxxxx"), + ("4'h7", BinaryOp::Mod, "4'h0", "4'bxxxx"), + ] { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal(left.to_string())), + op, + right: Box::new(ConstExpr::Literal(right.to_string())), + }; + let result = integral_literal_from_const_expr(&expr).unwrap(); + assert_eq!( + format_integral_literal_binary(&result), + expected, + "{left} {op:?} {right}" + ); + let reduced = ConstExpr::Unary { + op: UnaryOp::RedAnd, + expr: Box::new(expr), + }; + let result = integral_literal_from_const_expr(&reduced).unwrap(); + assert_eq!(result.width, 1); + assert!(!result.signed); + assert_eq!( + eval_const_expr(&reduced, &HashMap::default()), + integral_literal_as_i128(&result, false) + ); + } + } + + #[test] + fn preserves_four_state_reduction_truth_tables() { + let states = ['0', '1', 'x', 'z']; + for a in states { + for b in states { + for c in states { + let bits = [a, b, c]; + let unknown = bits.contains(&'x') || bits.contains(&'z'); + for (op, truth) in [ + ( + UnaryOp::RedAnd, + if bits.contains(&'0') { + Some(false) + } else if unknown { + None + } else { + Some(true) + }, + ), + ( + UnaryOp::RedOr, + if bits.contains(&'1') { + Some(true) + } else if unknown { + None + } else { + Some(false) + }, + ), + ( + UnaryOp::RedXor, + if unknown { + None + } else { + Some(bits.iter().filter(|&&bit| bit == '1').count() % 2 == 1) + }, + ), + ] { + let expr = ConstExpr::Unary { + op, + expr: Box::new(ConstExpr::Literal(format!("3'sb{a}{b}{c}"))), + }; + let result = integral_literal_from_const_expr(&expr) + .unwrap_or_else(|| panic!("{op:?} 3'sb{a}{b}{c}")); + assert_eq!( + result, + integral_literal_from_truth(truth), + "{op:?} 3'sb{a}{b}{c}" + ); + assert_eq!( + eval_const_expr(&expr, &HashMap::default()), + truth.map(i128::from) + ); + let complement = ConstExpr::Unary { + op: UnaryOp::BitNot, + expr: Box::new(expr), + }; + assert_eq!( + integral_literal_from_const_expr(&complement).unwrap(), + integral_literal_from_truth(truth.map(|value| !value)) + ); + assert_eq!( + eval_const_expr(&complement, &HashMap::default()), + truth.map(|value| i128::from(!value)) + ); + } + } + } + } + for op in [UnaryOp::RedAnd, UnaryOp::RedOr, UnaryOp::RedXor] { + for (operand, truth) in [ + ("'0", Some(false)), + ("'1", Some(true)), + ("'x", None), + ("'z", None), + ] { + let expr = ConstExpr::Unary { + op, + expr: Box::new(ConstExpr::Literal(operand.to_string())), + }; + assert_eq!( + integral_literal_from_const_expr(&expr).unwrap(), + integral_literal_from_truth(truth) + ); + assert_eq!( + eval_const_expr(&expr, &HashMap::default()), + truth.map(i128::from) + ); + } + } + } + #[test] fn parses_sized_based_literals() { let lit = parse_integral_literal("8'hf_f").unwrap(); @@ -1009,6 +1350,16 @@ mod literal_tests { let expr = ConstExpr::Literal("128'h80000000000000000000000000000000".to_string()); assert_eq!(eval_const_expr(&expr, &HashMap::default()), Some(i128::MIN)); + for op in [UnaryOp::Plus, UnaryOp::ToTwoState] { + let unary = ConstExpr::Unary { + op, + expr: Box::new(expr.clone()), + }; + assert_eq!( + eval_const_expr(&unary, &HashMap::default()), + Some(i128::MIN) + ); + } } #[test] @@ -1042,6 +1393,257 @@ mod literal_tests { assert_eq!(eval_const_expr(&ne, &HashMap::default()), Some(1)); } + #[test] + fn preserves_four_state_logical_truth_tables() { + let inputs = ["1'b0", "1'b1", "1'bx", "1'bz"]; + for (op, truth_table) in [ + (BinaryOp::LogicAnd, ["0000", "01xx", "0xxx", "0xxx"]), + (BinaryOp::LogicOr, ["01xx", "1111", "x1xx", "x1xx"]), + ] { + for (left_index, left) in inputs.iter().enumerate() { + for (right_index, right) in inputs.iter().enumerate() { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal((*left).to_string())), + op, + right: Box::new(ConstExpr::Literal((*right).to_string())), + }; + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + let expected = truth_table[left_index].as_bytes()[right_index] as char; + assert_eq!( + format_integral_literal_binary(&literal), + format!("1'b{expected}") + ); + } + } + } + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal("2'sb1x".to_string())), + op: BinaryOp::LogicAnd, + right: Box::new(ConstExpr::Literal("1'sb1".to_string())), + }; + assert_eq!(eval_const_expr(&expr, &HashMap::default()), Some(1)); + let literal = + eval_const_integral_literal_with_types(&expr, &HashMap::default(), &HashMap::default()) + .unwrap(); + assert_eq!(format_integral_literal_binary(&literal), "1'b1"); + } + + #[test] + fn preserves_four_state_equality_truth_tables() { + let inputs = ["1'b0", "1'b1", "1'bx", "1'bz"]; + for (op, truth_table) in [ + (BinaryOp::Eq, ["10xx", "01xx", "xxxx", "xxxx"]), + (BinaryOp::Ne, ["01xx", "10xx", "xxxx", "xxxx"]), + ] { + for (left_index, left) in inputs.iter().enumerate() { + for (right_index, right) in inputs.iter().enumerate() { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal((*left).to_string())), + op, + right: Box::new(ConstExpr::Literal((*right).to_string())), + }; + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap_or_else(|| panic!("failed to evaluate {expr:?}")); + let expected = truth_table[left_index].as_bytes()[right_index] as char; + assert_eq!( + format_integral_literal_binary(&literal), + format!("1'b{expected}"), + "{expr:?}" + ); + } + } + } + } + + #[test] + fn context_sizes_four_state_equality_operands() { + for (left, right, equality) in [ + ("2'b0x", "2'b1x", Some(false)), + ("2'bxz", "2'bzx", None), + ("1'sb1", "2'sb11", Some(true)), + ("1'sb1", "2'b11", Some(false)), + ("1'sbx", "2'sb1x", None), + ("1'sbx", "2'b1x", Some(false)), + ("8'hff", "'1", Some(true)), + ("'0", "8'b0000000x", None), + ("'x", "8'b0000000x", None), + ("129'bx", "129'b1", None), + ("129'bx", "129'b0", None), + ] { + for op in [BinaryOp::Eq, BinaryOp::Ne] { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal(left.to_string())), + op, + right: Box::new(ConstExpr::Literal(right.to_string())), + }; + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap_or_else(|| panic!("failed to evaluate {expr:?}")); + let expected = equality.map(|equal| equal ^ (op == BinaryOp::Ne)); + let expected_bit = expected.map_or('x', |equal| if equal { '1' } else { '0' }); + assert_eq!( + format_integral_literal_binary(&literal), + format!("1'b{expected_bit}"), + "{expr:?}" + ); + assert_eq!( + eval_const_expr(&expr, &HashMap::default()), + expected.map(i128::from), + "{expr:?}" + ); + } + } + } + + #[test] + fn preserves_four_state_relational_truth_tables() { + let inputs = ["1'b0", "1'b1", "1'bx", "1'bz"]; + for (op, truth_table) in [ + (BinaryOp::Lt, ["01xx", "00xx", "xxxx", "xxxx"]), + (BinaryOp::Le, ["11xx", "01xx", "xxxx", "xxxx"]), + (BinaryOp::Gt, ["00xx", "10xx", "xxxx", "xxxx"]), + (BinaryOp::Ge, ["10xx", "11xx", "xxxx", "xxxx"]), + ] { + for (left_index, left) in inputs.iter().enumerate() { + for (right_index, right) in inputs.iter().enumerate() { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal((*left).to_string())), + op, + right: Box::new(ConstExpr::Literal((*right).to_string())), + }; + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap_or_else(|| panic!("failed to evaluate {expr:?}")); + let expected = truth_table[left_index].as_bytes()[right_index] as char; + assert_eq!( + format_integral_literal_binary(&literal), + format!("1'b{expected}"), + "{expr:?}" + ); + } + } + } + } + + #[test] + fn context_sizes_four_state_relational_operands() { + for (left, right, ordering) in [ + ("2'b0x", "2'b1x", None), + ("2'b1z", "2'b00", None), + ("1'sb1", "2'sb00", Some(std::cmp::Ordering::Less)), + ("1'sb1", "2'b00", Some(std::cmp::Ordering::Greater)), + ("1'sb1", "2'sb11", Some(std::cmp::Ordering::Equal)), + ("8'hff", "'1", Some(std::cmp::Ordering::Equal)), + ( + "129'sh1ffffffffffffffffffffffffffffffff", + "129'sb0", + Some(std::cmp::Ordering::Less), + ), + ("129'bx", "129'b0", None), + ] { + for op in [BinaryOp::Lt, BinaryOp::Le, BinaryOp::Gt, BinaryOp::Ge] { + let expr = ConstExpr::Binary { + left: Box::new(ConstExpr::Literal(left.to_string())), + op, + right: Box::new(ConstExpr::Literal(right.to_string())), + }; + let expected = ordering.map(|ordering| match op { + BinaryOp::Lt => ordering.is_lt(), + BinaryOp::Le => ordering.is_le(), + BinaryOp::Gt => ordering.is_gt(), + BinaryOp::Ge => ordering.is_ge(), + _ => unreachable!(), + }); + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap_or_else(|| panic!("failed to evaluate {expr:?}")); + let expected_bit = expected.map_or('x', |value| if value { '1' } else { '0' }); + assert_eq!( + format_integral_literal_binary(&literal), + format!("1'b{expected_bit}"), + "{expr:?}" + ); + assert_eq!( + eval_const_expr(&expr, &HashMap::default()), + expected.map(i128::from), + "{expr:?}" + ); + } + } + } + + #[test] + fn context_sizes_known_constant_mux_arms() { + for (condition, then_arm, else_arm, expected) in [ + ("1'b1", "1'sb1", "2'sb00", "2'sb11"), + ("1'b0", "2'sb00", "1'sb1", "2'sb11"), + ("1'b1", "1'sb1", "2'b00", "2'b01"), + ("1'b1", "1'sbx", "2'sb00", "2'sbxx"), + ("1'b1", "1'sbz", "2'b00", "2'b0z"), + ("1'b1", "'1", "2'b00", "2'b11"), + ] { + let expr = ConstExpr::Mux { + condition: Box::new(ConstExpr::Literal(condition.to_string())), + then_expr: Box::new(ConstExpr::Literal(then_arm.to_string())), + else_expr: Box::new(ConstExpr::Literal(else_arm.to_string())), + }; + let literal = eval_const_integral_literal_with_types( + &expr, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(format_integral_literal_binary(&literal), expected); + } + } + + #[test] + fn merges_constant_mux_arms_without_losing_four_state_masks() { + let merged = ConstExpr::Mux { + condition: Box::new(ConstExpr::Literal("1'bx".to_string())), + then_expr: Box::new(ConstExpr::Literal("1'b0".to_string())), + else_expr: Box::new(ConstExpr::Literal("1'b1".to_string())), + }; + let identical_z = ConstExpr::Mux { + condition: Box::new(ConstExpr::Literal("1'bx".to_string())), + then_expr: Box::new(ConstExpr::Literal("1'bz".to_string())), + else_expr: Box::new(ConstExpr::Literal("1'bz".to_string())), + }; + + let merged = eval_const_integral_literal_with_types( + &merged, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(format_integral_literal_binary(&merged), "1'bx"); + let identical_z = eval_const_integral_literal_with_types( + &identical_z, + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(format_integral_literal_binary(&identical_z), "1'bz"); + } + #[test] fn context_sizes_fills_in_constant_case_equality() { let eq = ConstExpr::Binary { diff --git a/crates/celox/tests/enum_type.rs b/crates/celox/tests/enum_type.rs index 5b618ef0c..ca5cceff3 100644 --- a/crates/celox/tests/enum_type.rs +++ b/crates/celox/tests/enum_type.rs @@ -7,7 +7,6 @@ mod test_utils; all_backends! { fn test_enum_case_match(sim) { - @ignore_on(sv); @setup { let code = r#" module Top ( sel: input logic<2>, @@ -47,7 +46,6 @@ default: o = 8'h01; } fn test_enum_ff_state_machine(sim) { - @ignore_on(sv); @setup { let code = r#" module Top ( clk: input clock, @@ -125,7 +123,6 @@ assign state_out = state; // Enum-typed variables can be assigned from logic inputs // and compared against enum members. fn test_enum_assign_and_compare(sim) { - @ignore_on(sv); @setup { let code = r#" module Top ( diff --git a/crates/celox/tests/frontends/systemverilog/always_comb.rs b/crates/celox/tests/frontends/systemverilog/always_comb.rs index a8dcda734..f5a27d266 100644 --- a/crates/celox/tests/frontends/systemverilog/always_comb.rs +++ b/crates/celox/tests/frontends/systemverilog/always_comb.rs @@ -34,3 +34,46 @@ sv_backends! { assert_eq!(sim.get(z), 0xdau8.into()); } } + +sv_backends! { + fn preserves_intervening_reads_when_merging_conditional_writes(sim) { + @setup { + let sv = r#" + module Top( + input logic c, + input logic d, + output logic x, + output logic y + ); + always_comb begin + x = d; + y = x; + if (c) x = 1'b1; + end + endmodule + "#; + } + @build Simulator::from_sv_sources(vec![(sv, Path::new("always_comb_order.sv"))], "Top"); + + let c = sim.signal("c"); + let d = sim.signal("d"); + let x = sim.signal("x"); + let y = sim.signal("y"); + + sim.modify(|io| { + io.set(c, 1u8); + io.set(d, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 0u8.into()); + + sim.modify(|io| { + io.set(c, 0u8); + io.set(d, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 1u8.into()); + } +} diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 9b1b533c5..216419229 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -55,6 +55,177 @@ fn rejects_same_vector_slice_read_before_write_in_always_comb() { ); } +#[test] +fn rejects_self_reads_hidden_in_comb_function_calls() { + let error = cranelift_build_error( + r#" + module Top(input logic c, output logic x); + function automatic logic read_x(); + return x; + endfunction + always_comb begin + if (c) + x = read_x(); + else + x = 1'b0; + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn rejects_block_local_declarations_inside_always_comb() { + let error = cranelift_build_error( + r#" + module Top(input logic c, a, b, output logic t, y); + always_comb begin + t = 1'b0; + if (c) begin + logic t; + t = a; + y = t; + end else begin + y = b; + end + end + endmodule + "#, + ); + assert!( + error.contains("block-local declaration inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn substitutes_prior_comb_values_into_function_bodies() { + let source = r#" + module Top(input logic c, output logic x, y); + function automatic logic read_x(); + return x; + endfunction + always_comb begin + x = 1'b0; + y = read_x(); + if (c) + x = 1'b1; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("blocking_read_inside_function.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let x = sim.signal("x"); + let y = sim.signal("y"); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn substitutes_blocking_values_inside_dynamic_select_indices() { + let source = r#" + module Top( + input logic c, a, b, + input logic [7:0] lut[2], + output logic x, + output logic [7:0] y + ); + always_comb begin + x = a; + y = lut[x]; + if (c) + x = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("blocking_dynamic_select_index.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let lut = sim.signal("lut"); + let x = sim.signal("x"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 1u8); + io.set(b, 0u8); + io.set(lut, 0xaa55u16); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 0xaau8.into()); + sim.modify(|io| { + io.set(c, 1u8); + io.set(a, 0u8); + io.set(b, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 0x55u8.into()); +} + +#[test] +fn substitutes_blocking_values_inside_dynamic_write_indices() { + let source = r#" + module Top( + input logic c, + input logic [1:0] a, + input logic [1:0] d, + output logic [3:0] y + ); + logic bits[4]; + logic [1:0] index; + always_comb begin + bits = '0; + index = a; + bits[index] = 1'b1; + if (c) + index = d; + end + assign y = {bits[3], bits[2], bits[1], bits[0]}; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("blocking_dynamic_write_index.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let d = sim.signal("d"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, false); + io.set(a, 1u8); + io.set(d, 3u8); + }) + .unwrap(); + assert_eq!(sim.get(y), 0b0010u8.into()); + sim.modify(|io| { + io.set(c, true); + io.set(a, 0u8); + io.set(d, 2u8); + }) + .unwrap(); + assert_eq!(sim.get(y), 0b0001u8.into()); +} + #[test] fn rejects_inline_enum_ports_instead_of_scalarizing_them() { let error = cranelift_build_error( @@ -1380,6 +1551,114 @@ fn composes_dynamic_array_writes_after_whole_array_assignments() { assert_eq!(sim.get(cleared), 0u8.into()); } +#[test] +fn preserves_dynamic_selected_writes_before_conditional_whole_writes() { + let source = r#" + module Top( + input logic [1:0] index, + input logic data, + input logic replace, + output logic [3:0] value + ); + always_comb begin + value = '0; + value[index] = data; + if (replace) value = '1; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("dynamic_select_before_conditional_whole_write.sv"), + )], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let index = sim.signal("index"); + let data = sim.signal("data"); + let replace = sim.signal("replace"); + let value = sim.signal("value"); + + sim.modify(|io| { + io.set(index, 2u8); + io.set(data, true); + io.set(replace, false); + }) + .unwrap(); + assert_eq!(sim.get(value), 0b0100u8.into()); + + sim.modify(|io| io.set(replace, true)).unwrap(); + assert_eq!(sim.get(value), 0b1111u8.into()); + + sim.modify(|io| { + io.set_four_state(index, BigUint::default(), BigUint::from(0b11u8)); + io.set(replace, false); + }) + .unwrap(); + assert_eq!( + sim.get_four_state(value), + (BigUint::default(), BigUint::default()) + ); +} + +#[test] +fn preserves_dynamic_array_writes_before_conditional_whole_writes() { + let source = r#" + module Top( + input logic [1:0] index, + input logic [7:0] data, + input logic replace, + output logic [7:0] value + ); + logic [7:0] values[4]; + always_comb begin + values = '0; + values[index] = data; + if (replace) values = '1; + end + assign value = values[2]; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("dynamic_array_before_conditional_whole_write.sv"), + )], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let index = sim.signal("index"); + let data = sim.signal("data"); + let replace = sim.signal("replace"); + let value = sim.signal("value"); + + sim.modify(|io| { + io.set(index, 2u8); + io.set(data, 0xa5u8); + io.set(replace, false); + }) + .unwrap(); + assert_eq!(sim.get(value), 0xa5u8.into()); + + sim.modify(|io| io.set(replace, true)).unwrap(); + assert_eq!(sim.get(value), 0xffu8.into()); + + sim.modify(|io| { + io.set_four_state(index, BigUint::default(), BigUint::from(0b11u8)); + io.set(replace, false); + }) + .unwrap(); + assert_eq!( + sim.get_four_state(value), + (BigUint::default(), BigUint::default()) + ); +} + #[test] fn ignores_unknown_dynamic_array_write_indices() { let source = r#" @@ -2118,6 +2397,52 @@ fn produces_unknown_for_four_state_division_by_zero() { ); } +#[test] +fn rejects_incomplete_cases_for_potential_two_state_division_by_zero() { + let error = cranelift_build_error( + r#" + module Top(input bit a, b, output logic y); + always_comb begin + case (a / b) + 1'b0: y = 1'b0; + 1'b1: y = 1'b1; + endcase + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn recognizes_complete_cases_for_nonzero_two_state_divisors() { + let source = r#" + module Top(input bit a, output logic y); + always_comb begin + case (a / 1'b1) + 1'b0: y = 1'b0; + 1'b1: y = 1'b1; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("nonzero_two_state_case_divisor.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let y = sim.signal("y"); + sim.modify(|io| io.set(a, false)).unwrap(); + assert_eq!(sim.get(y), false.into()); + sim.modify(|io| io.set(a, true)).unwrap(); + assert_eq!(sim.get(y), true.into()); +} + #[test] fn preserves_typedef_function_return_width_in_ff_case() { let source = r#" @@ -2223,48 +2548,1804 @@ fn propagates_assignment_width_into_combinational_expressions() { } #[test] -fn preserves_last_write_order_inside_always_comb() { - let source = r#" - module Top(input logic a, b, output logic y); +fn preserves_last_write_order_inside_always_comb() { + let source = r#" + module Top(input logic a, b, output logic y); + always_comb begin + y = a; + y = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources(vec![(source, Path::new("comb_order.sv"))], "Top") + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(a, 1u8); + io.set(b, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn merges_overlapping_writes_inside_always_comb() { + let source = r#" + module Top(input logic a, output logic [7:0] y); + always_comb begin + y = 8'h00; + y[0] = a; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources(vec![(source, Path::new("comb_overlap.sv"))], "Top") + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let y = sim.signal("y"); + sim.modify(|io| io.set(a, 1u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); + sim.modify(|io| io.set(a, 0u8)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn preserves_prior_conditional_writes_across_overlapping_comb_slices() { + let source = r#" + module Top(input logic c, d, output logic [3:0] x); + always_comb begin + x = '0; + if (c) x[3:1] = 3'b111; + if (d) x[2:0] = 3'b000; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_overlapping_slice_fallback.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let d = sim.signal("d"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 1u8); + io.set(d, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 0b1110u8.into()); +} + +#[test] +fn later_exhaustive_comb_chain_overrides_an_earlier_chain() { + let source = r#" + module Top(input logic c, d, a, b, e, f, output logic x); + always_comb begin + if (c) x = a; + else x = b; + if (d) x = e; + else x = f; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_consecutive_exhaustive_chains.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let d = sim.signal("d"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let e = sim.signal("e"); + let f = sim.signal("f"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 1u8); + io.set(d, 0u8); + io.set(a, 1u8); + io.set(b, 1u8); + io.set(e, 1u8); + io.set(f, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn preserves_selected_values_before_conditional_whole_comb_writes() { + let source = r#" + module Top(input logic c, a, output logic [7:0] x); + always_comb begin + x = '0; + x[0] = a; + if (c) x = 8'hff; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_selected_then_whole.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); +} + +#[test] +fn reads_values_assigned_earlier_on_each_comb_branch() { + let source = r#" + module Top(input logic c, output logic x, y); + always_comb begin + if (c) begin + x = 1'b1; + y = x; + end else begin + x = 1'b0; + y = x; + end + end + endmodule + "#; + let mut sim = + Simulator::from_sv_sources(vec![(source, Path::new("comb_path_local_read.sv"))], "Top") + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let x = sim.signal("x"); + let y = sim.signal("y"); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + assert_eq!(sim.get(y), 1u8.into()); + sim.modify(|io| io.set(c, 0u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn freezes_comb_branch_guards_before_predicate_writes() { + let source = r#" + module Top(input logic en, output logic t, y); + always_comb begin + t = en; + y = 1'b0; + if (t) begin + t = 1'b0; + y = 1'b1; + end + end + endmodule + "#; + let mut sim = + Simulator::from_sv_sources(vec![(source, Path::new("comb_frozen_predicate.sv"))], "Top") + .build_cranelift() + .unwrap(); + let en = sim.signal("en"); + let t = sim.signal("t"); + let y = sim.signal("y"); + sim.modify(|io| io.set(en, 1u8)).unwrap(); + assert_eq!(sim.get(t), 0u8.into()); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn freezes_all_sibling_comb_guards_before_branch_writes() { + let source = r#" + module Top(input logic en, output logic s, y); + always_comb begin + s = en; + y = 1'b0; + if (s) + s = 1'b0; + else if (!s) + y = 1'b1; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_sibling_frozen_predicate.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let en = sim.signal("en"); + let s = sim.signal("s"); + let y = sim.signal("y"); + sim.modify(|io| io.set(en, 1u8)).unwrap(); + assert_eq!(sim.get(s), 0u8.into()); + assert_eq!(sim.get(y), 0u8.into()); + sim.modify(|io| io.set(en, 0u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn propagates_nested_comb_writes_to_the_enclosing_path() { + let source = r#" + module Top(input logic c, d, output logic [1:0] x, y); + always_comb begin + if (c) begin + x = 2'd1; + if (d) x = 2'd2; + y = x; + end else begin + x = 2'd0; + y = x; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_nested_path_value.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let d = sim.signal("d"); + let x = sim.signal("x"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, 1u8); + io.set(d, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 2u8.into()); + assert_eq!(sim.get(y), 2u8.into()); +} + +#[test] +fn substitutes_whole_vector_reads_after_selected_comb_writes() { + let source = r#" + module Top(input logic c, a, b, output logic [7:0] x, y); + always_comb begin + x = '0; + x[0] = a; + y = x; + if (c) x[0] = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_selected_then_whole_read.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let x = sim.signal("x"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, 1u8); + io.set(a, 1u8); + io.set(b, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 0u8.into()); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn reevaluates_constant_casts_after_parameter_overrides() { + let source = r#" + module Top #(parameter A = 3) (output logic [7:0] y); + typedef logic [7:0] byte_t; + localparam B = byte_t'(A); + assign y = B; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("parameter_dependent_cast.sv"))], + "Top", + ) + .param("A", 4) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 4u8.into()); +} + +#[test] +fn resolves_typedefs_in_size_function_cast_targets() { + let source = r#" + module Top(output logic [7:0] y); + typedef logic [7:0] byte_t; + localparam P = $bits(byte_t)'(16'h1ff); + assign y = P; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("typedef_size_function_cast.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0xffu8.into()); +} + +#[test] +fn resolves_parameterized_direct_types_in_size_function_cast_targets() { + let source = r#" + module Top(output logic [7:0] y); + parameter W = 8; + localparam Q = $bits(logic [W'(7):0])'(8'hff); + assign y = Q; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("direct_type_size_function_cast.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0xffu8.into()); +} + +#[test] +fn infers_parameter_expression_widths_in_size_function_cast_targets() { + let source = r#" + module Top(output logic [7:0] y); + parameter logic [7:0] P = 0; + localparam Q = $bits(P)'(4'hf); + assign y = Q; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("expression_size_function_cast.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0x0fu8.into()); +} + +#[test] +fn infers_variable_widths_in_size_function_cast_targets() { + let source = r#" + module Top( + input logic [7:0] a, + output logic [7:0] port_bits, + output logic [7:0] port_size, + output logic [31:0] signal_bits, + output logic [7:0] signal_size + ); + logic [7:0] internal [0:3]; + localparam PB = $bits(a)'(12'h1ff); + localparam PS = $size(a)'(12'h1ff); + localparam SB = $bits(internal)'(40'h1fffffffff); + localparam SS = $size(internal)'(8'h1f); + assign port_bits = PB; + assign port_size = PS; + assign signal_bits = SB; + assign signal_size = SS; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("variable_size_function_cast.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("port_bits")), 0xffu8.into()); + assert_eq!(sim.get(sim.signal("port_size")), 0xffu8.into()); + assert_eq!(sim.get(sim.signal("signal_bits")), 0xffff_ffffu32.into()); + assert_eq!(sim.get(sim.signal("signal_size")), 0x0fu8.into()); +} + +#[test] +fn recognizes_exhaustive_comb_coverage_across_selected_writes() { + let source = r#" + module Top(input logic c, a, b, output logic [1:0] x); + always_comb begin + if (c) + x = 2'b00; + else begin + x[1] = a; + x[0] = b; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_overlapping_branch_coverage.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 1u8); + io.set(b, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 2u8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn reevaluates_constant_casts_with_enum_operands() { + let source = r#" + module Top(output logic [7:0] y); + typedef logic [7:0] byte_t; + typedef enum logic [1:0] { N = 2 } E; + localparam B = byte_t'(N); + assign y = B; + endmodule + "#; + let mut sim = + Simulator::from_sv_sources(vec![(source, Path::new("enum_dependent_cast.sv"))], "Top") + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 2u8.into()); +} + +#[test] +fn coerces_each_guarded_rhs_before_building_a_mux() { + let source = r#" + module Top(input logic c, output logic [7:0] x); + always_comb begin + if (c) + x = 1'sb1; + else + x = 8'b0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("comb_guarded_assignment_coercion.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let x = sim.signal("x"); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0xffu8.into()); + sim.modify(|io| io.set(c, 0u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn coerces_unconditional_fallbacks_before_building_a_mux() { + let source = r#" + module Top(input logic c, output logic [7:0] x); + always_comb begin + x = 1'sb1; + if (c) + x = 8'b0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("comb_unconditional_assignment_coercion.sv"), + )], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let x = sim.signal("x"); + sim.modify(|io| io.set(c, 0u8)).unwrap(); + assert_eq!(sim.get(x), 0xffu8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn sizes_unpacked_array_type_function_cast_targets() { + let source = r#" + module Top(output logic [31:0] bits_y, output logic [7:0] size_y); + typedef logic [7:0] bytes_t [0:3]; + localparam B = $bits(bytes_t)'(40'h1fffffffff); + localparam S = $size(bytes_t)'(8'h1f); + assign bits_y = B; + assign size_y = S; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("unpacked_typedef_size_function_cast.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("bits_y")), 0xffff_ffffu32.into()); + assert_eq!(sim.get(sim.signal("size_y")), 0x0fu8.into()); +} + +#[test] +fn resolves_parameter_ranges_with_enum_constants() { + let source = r#" + module Top(output logic [7:0] y); + typedef enum logic [1:0] { W = 2 } E; + localparam logic signed [W-1:0] P = 2'b11; + assign y = P; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("enum_dependent_parameter_range.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0xffu8.into()); +} + +#[test] +fn resolves_named_casts_in_function_return_ranges() { + let source = r#" + module Top #(parameter W = 8) (output logic [7:0] y); + function automatic logic [W'(7):0] f(); + return 8'hff; + endfunction + always_comb y = f(); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("named_cast_function_return_range.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0xffu8.into()); +} + +#[test] +fn context_sizes_wide_unbased_fills_in_conditional_writes() { + let source = r#" + module Top(input logic c, output logic [63:0] y); + always_comb begin + if (c) y = '1; + else y = '0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("wide_conditional_unbased_fill.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + sim.set(sim.signal("c"), 1u8); + assert_eq!(sim.get(sim.signal("y")), u64::MAX.into()); + sim.set(sim.signal("c"), 0u8); + assert_eq!(sim.get(sim.signal("y")), 0u64.into()); +} + +#[test] +fn preserves_enum_dependent_parameters_in_instance_overrides() { + let source = r#" + module Child #(parameter Q = 0) (output logic [1:0] y); + assign y = Q; + endmodule + module Top(output logic [1:0] y); + typedef enum logic [1:0] { A = 2 } E; + localparam P = A; + Child #(.Q(P)) child(.y(y)); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("enum_dependent_instance_parameter.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 2u8.into()); +} + +#[test] +fn translates_declared_packed_indices_when_composing_whole_writes() { + let source = r#" + module Top( + input logic c, + input logic b, + input logic [3:0] descending_value, + input logic [3:0] ascending_value, + output logic [4:1] descending_x, + output logic [1:4] ascending_x + ); + always_comb begin + descending_x = descending_value; + if (c) + descending_x[2] = b; + end + always_comb begin + ascending_x = ascending_value; + if (c) + ascending_x[1] = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("declared_packed_comb_coordinates.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let b = sim.signal("b"); + let descending_value = sim.signal("descending_value"); + let ascending_value = sim.signal("ascending_value"); + let descending_x = sim.signal("descending_x"); + let ascending_x = sim.signal("ascending_x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(b, 0u8); + io.set(descending_value, 0b0010u8); + io.set(ascending_value, 0b1000u8); + }) + .unwrap(); + assert_eq!(sim.get(descending_x), 0b0010u8.into()); + assert_eq!(sim.get(ascending_x), 0b1000u8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(descending_x), 0u8.into()); + assert_eq!(sim.get(ascending_x), 0u8.into()); +} + +#[test] +fn types_earlier_enum_members_in_later_initializers() { + let source = r#" + module Top(output logic y); + typedef enum logic signed [1:0] { + A = 2'b10, + B = (A < 0) + } E; + assign y = B; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("typed_enum_member_initializer.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 1u8.into()); +} + +#[test] +fn converts_two_state_blocking_writes_before_intervening_reads() { + let source = r#" + module Top(input logic c, output logic y); + bit x; + always_comb begin + x = 1'bx; + y = x; + if (c) + x = 1'b1; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("two_state_intervening_comb_read.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let y = sim.signal("y"); + sim.modify(|io| io.set(c, 0u8)).unwrap(); + assert_eq!( + sim.get_four_state(y), + (BigUint::from(0u8), BigUint::from(0u8)) + ); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!( + sim.get_four_state(y), + (BigUint::from(0u8), BigUint::from(0u8)) + ); +} + +#[test] +fn accepts_constant_true_always_comb_guards() { + let source = r#" + module Top(input logic a, output logic x); + always_comb + if (1'b1) + x = a; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("constant_true_comb_guard.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let x = sim.signal("x"); + sim.modify(|io| io.set(a, 0u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); + sim.modify(|io| io.set(a, 1u8)).unwrap(); + assert_eq!(sim.get(x), 1u8.into()); +} + +#[test] +fn recognizes_constant_true_nested_comb_guards_as_definite() { + let source = r#" + module Top(input logic c, a, b, output logic y); + always_comb begin + if (c) begin + if (1'b1) + y = a; + end else begin + y = b; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("constant_true_nested_comb_guard.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, false); + io.set(a, false); + io.set(b, true); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| { + io.set(c, true); + io.set(a, false); + }) + .unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn treats_constant_unknown_nested_comb_guards_as_false() { + let source = r#" + module Top(input logic c, a, b, output logic y); + always_comb begin + if (c) begin + if (1'bx) + ; + else + y = a; + end else begin + y = b; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("constant_unknown_nested_comb_guard.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, false); + io.set(a, false); + io.set(b, true); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| { + io.set(c, true); + io.set(a, false); + }) + .unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn recognizes_complementary_guarded_writes_as_definite() { + let source = r#" + module Top( + input logic c, a, b, e, + input bit d, + output logic x + ); + always_comb begin + if (c) + x = a; + else begin + if (d) + x = b; + if (!d) + x = e; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("complementary_guarded_comb_writes.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let d = sim.signal("d"); + let e = sim.signal("e"); + let x = sim.signal("x"); + + sim.modify(|io| { + io.set(c, true); + io.set(a, true); + io.set(b, false); + io.set(d, false); + io.set(e, false); + }) + .unwrap(); + assert_eq!(sim.get(x), true.into()); + + sim.modify(|io| { + io.set(c, false); + io.set(d, true); + }) + .unwrap(); + assert_eq!(sim.get(x), false.into()); + + sim.modify(|io| { + io.set(d, false); + io.set(e, true); + }) + .unwrap(); + assert_eq!(sim.get(x), true.into()); +} + +#[test] +fn does_not_combine_guards_across_condition_writes() { + let error = cranelift_build_error( + r#" + module Top(input bit s, input logic a, b, output logic x); + bit d; + always_comb begin + d = s; + if (d) + x = a; + d = !d; + if (!d) + x = b; + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn registers_enum_types_with_aliased_bases() { + let source = r#" + module Top(output logic [1:0] y); + typedef logic [1:0] base_t; + typedef enum base_t { A = 2'd2 } E; + E state; + always_comb state = A; + assign y = state; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("aliased_enum_base_type.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 2u8.into()); +} + +#[test] +fn collects_enum_constants_from_parameterized_aliased_bases() { + let source = r#" + module Top #(parameter W = 2) (output logic [1:0] y); + typedef logic [W'(1):0] B; + typedef enum B { A = 2'b10 } E; + assign y = A; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("parameterized_aliased_enum_base.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 2u8.into()); +} + +#[test] +fn collects_enum_constants_from_parameterized_direct_bases() { + let source = r#" + module Top #(parameter W = 2) (output logic [3:0] y); + typedef enum logic [W'(2'd3):0] { A = 4'b1010 } E; + assign y = A; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("parameterized_direct_enum_base.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 0xau8.into()); +} + +#[test] +fn preserves_masks_in_compound_constant_cast_operands() { + let source = r#" + module Top(output logic [3:0] y); + localparam logic [3:0] Q = 4'(2'bx1 | 2'b00); + assign y = Q; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("compound_masked_constant_cast.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + assert_eq!( + sim.get_four_state(sim.signal("y")), + (BigUint::from(0b0011u8), BigUint::from(0b0010u8)) + ); +} + +#[test] +fn preserves_body_parameter_overrides_during_enum_collection() { + let source = r#" + module Child(output logic y); + parameter P = 0; + typedef enum logic { A = P } E; + assign y = A; + endmodule + module Top(output logic y); + Child #(.P(1)) child(.y(y)); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("body_parameter_override_enum.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), true.into()); +} + +#[test] +fn coerces_selected_writes_before_whole_vector_normalization() { + let source = r#" + module Top(input logic c, output logic [7:0] x); + always_comb begin + x = '0; + x[3:1] = 1'sb1; + if (c) + x = 8'b0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("selected_write_before_whole_normalization.sv"), + )], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let x = sim.signal("x"); + sim.modify(|io| io.set(c, 0u8)).unwrap(); + assert_eq!(sim.get(x), 0x0eu8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn preserves_four_state_masks_through_constant_casts() { + let source = r#" + module Top(output logic y); + typedef logic [1:0] two_t; + localparam logic [1:0] PX = two_t'(1'bx); + localparam logic [1:0] PZ = two_t'(1'bz); + assign y = (PX === 2'b0x) && (PZ === 2'b0z); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("four_state_constant_cast.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 1u8.into()); +} + +#[test] +fn expands_unbased_fill_literals_to_constant_cast_widths() { + let source = r#" + module Top(output logic y); + typedef logic [63:0] wide_t; + localparam logic [63:0] P1 = wide_t'('1); + localparam logic [63:0] PX = wide_t'('x); + localparam logic [63:0] PZ = wide_t'('z); + assign y = (P1 === 64'hffffffffffffffff) + && (PX === 64'hxxxxxxxxxxxxxxxx) + && (PZ === 64'hzzzzzzzzzzzzzzzz); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("unbased_fill_constant_cast.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 1u8.into()); +} + +#[test] +fn accepts_selected_writes_killed_by_later_whole_writes() { + let source = r#" + module Top(input logic c, a, output logic [1:0] x); + always_comb begin + if (c) + x[0] = a; + x = '0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("killed_selected_comb_write.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 0u8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0u8.into()); +} + +#[test] +fn recognizes_complete_cases_over_two_state_selectors() { + let source = r#" + module Top(input bit s, output logic y); + always_comb begin + case (s) + 1'b0: y = 1'b0; + 1'b1: y = 1'b1; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("complete_two_state_case.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let s = sim.signal("s"); + let y = sim.signal("y"); + sim.modify(|io| io.set(s, 0u8)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); + sim.modify(|io| io.set(s, 1u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn recognizes_complete_cases_over_constant_selectors() { + let source = r#" + module Top(input logic a, output logic y); + localparam logic P = 1'b0; + always_comb begin + case (P) + 1'b0: y = a; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("complete_constant_case.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let y = sim.signal("y"); + sim.modify(|io| io.set(a, true)).unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(a, false)).unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn recognizes_identical_two_state_conditional_case_arms() { + let source = r#" + module Top(input logic c, a, output logic y); + always_comb begin + case (c ? 1'b0 : 1'b0) + 1'b0: y = a; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("identical_conditional_case_arms.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, true); + io.set(a, true); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); +} + +#[test] +fn recognizes_complete_cases_over_two_state_function_results() { + let source = r#" + module Top(input bit s, input logic a, b, output logic y); + function automatic bit select(); + return s; + endfunction + always_comb begin + case (select()) + 1'b0: y = a; + 1'b1: y = b; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("complete_two_state_function_case.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let s = sim.signal("s"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(s, false); + io.set(a, true); + io.set(b, false); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(s, true)).unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn skips_unreachable_duplicate_items_in_complete_two_state_cases() { + let source = r#" + module Top(input bit s, input logic a, b, output logic y); + always_comb begin + case (s) + 1'b0: y = a; + 1'b1: y = b; + 1'b0: ; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("unreachable_duplicate_case_item.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let s = sim.signal("s"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(s, false); + io.set(a, true); + io.set(b, false); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(s, true)).unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn rejects_incomplete_cases_for_potentially_invalid_two_state_selects() { + let error = cranelift_build_error( + r#" + module Top(input bit [1:0] a, input bit [2:0] i, output logic y); + always_comb begin + case (a[i]) + 1'b0: y = 1'b0; + 1'b1: y = 1'b1; + endcase + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn regroups_comb_targets_after_dynamic_index_substitution() { + let error = cranelift_build_error( + r#" + module Top( + input logic c, a, b, + output logic [1:0] x + ); + logic i; + always_comb begin + if (c) begin + i = 1'b0; + x[i] = a; + end else begin + i = 1'b1; + x[i] = b; + end + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn recognizes_complete_cases_over_two_state_expressions() { + let source = r#" + module Top(input bit a, b, output logic y); + always_comb begin + case ({a, b}) + 2'b00: y = 1'b0; + 2'b01: y = 1'b1; + 2'b10: y = 1'b1; + 2'b11: y = 1'b0; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("complete_two_state_expression_case.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(a, 0u8); + io.set(b, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(y), 0u8.into()); + sim.modify(|io| io.set(b, 1u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); + sim.modify(|io| io.set(a, 1u8)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn respects_signed_case_item_sizing_when_checking_coverage() { + let error = cranelift_build_error( + r#" + module Top(input bit signed [1:0] s, output logic y); + always_comb begin + case (s) + 0: y = 1'b0; + 1: y = 1'b1; + 2: y = 1'b0; + 3: y = 1'b1; + endcase + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn recognizes_complete_signed_cases_with_sized_labels() { + let source = r#" + module Top(input bit signed [1:0] s, output logic y); + always_comb begin + case (s) + 2'sb00: y = 1'b0; + 2'sb01: y = 1'b1; + 2'sb10: y = 1'b0; + 2'sb11: y = 1'b1; + endcase + end + endmodule + "#; + let mut sim = + Simulator::from_sv_sources(vec![(source, Path::new("complete_signed_case.sv"))], "Top") + .build_cranelift() + .unwrap(); + let s = sim.signal("s"); + let y = sim.signal("y"); + sim.modify(|io| io.set(s, 2u8)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); + sim.modify(|io| io.set(s, 3u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn recognizes_nested_complete_cases_as_definite_assignments() { + let source = r#" + module Top(input logic c, input bit s, output logic x); + always_comb begin + if (c) + case (s) + 1'b0: x = 1'b0; + 1'b1: x = 1'b1; + endcase + else + x = 1'b0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("nested_complete_two_state_case.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let s = sim.signal("s"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(s, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 0u8.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 1u8.into()); +} + +#[test] +fn excludes_unreachable_case_arms_from_definite_assignments() { + let source = r#" + module Top( + input logic c, + input bit s, + input logic a, b, d, + output logic y + ); + always_comb begin + if (c) + case (s) + 1'b0: y = a; + 1'b1: y = b; + 2'b10: ; + default: ; + endcase + else + y = d; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("nested_complete_case_unreachable_arms.sv"), + )], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let s = sim.signal("s"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let d = sim.signal("d"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, true); + io.set(s, false); + io.set(a, true); + io.set(b, false); + io.set(d, false); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(s, true)).unwrap(); + assert_eq!(sim.get(y), false.into()); + sim.modify(|io| { + io.set(c, false); + io.set(d, true); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); +} + +#[test] +fn registers_enum_types_with_default_bases() { + let source = r#" + module Top(output logic [31:0] y); + typedef enum { Idle = 0, Run = 1 } State; + State state; + always_comb state = Run; + assign y = state; + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("default_enum_base_type.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 1u8.into()); +} + +#[test] +fn substitutes_enum_members_after_expanding_instance_connection_functions() { + let source = r#" + module Child(input logic [1:0] x, output logic [1:0] y); + assign y = x; + endmodule + + module Top(output logic [1:0] y); + typedef enum logic [1:0] { A = 2'b10 } E; + function automatic logic [1:0] f(); + return A; + endfunction + Child child(.x(f()), .y(y)); + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("enum_function_instance_connection.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + assert_eq!(sim.get(sim.signal("y")), 2u8.into()); +} + +#[test] +fn rejects_ranged_enum_members_instead_of_registering_the_base_name() { + let error = cranelift_build_error( + r#" + module Top(output logic [31:0] y); + typedef enum int { S[2] = 4 } E; + assign y = S0; + endmodule + "#, + ); + assert!( + error.contains("ranged enum member"), + "unexpected error: {error}" + ); +} + +#[test] +fn rejects_enum_initializers_that_do_not_fit_the_base_type() { + let error = cranelift_build_error( + r#" + module Top(output logic [1:0] y); + typedef enum logic [1:0] { A = 3'd4 } E; + assign y = A; + endmodule + "#, + ); + assert!( + error.contains("enum member `A` value does not fit its base type"), + "unexpected error: {error}" + ); +} + +#[test] +fn preserves_masked_parameter_guards_before_latch_detection() { + let source = r#" + module Top(input logic a, output logic y); + localparam logic [1:0] S = 2'bx1; + always_comb begin + case (S) + 2'bx1: y = a; + endcase + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("masked_parameter_case_guard.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let a = sim.signal("a"); + let y = sim.signal("y"); + sim.modify(|io| io.set(a, 0u8)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); + sim.modify(|io| io.set(a, 1u8)).unwrap(); + assert_eq!(sim.get(y), 1u8.into()); +} + +#[test] +fn coerces_whole_unpacked_array_writes_to_the_flattened_width() { + let source = r#" + module Top( + input logic c, + input logic signed [7:0] a[2], b[2], + output logic [7:0] x[2] + ); + always_comb begin + if (c) + x = a; + else + x = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("whole_unpacked_array_comb_write.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 0x2211u16); + io.set(b, 0x4433u16); + }) + .unwrap(); + assert_eq!(sim.get(x), 0x4433u16.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0x2211u16.into()); +} + +#[test] +fn normalizes_element_writes_before_conditional_whole_array_writes() { + let source = r#" + module Top( + input logic c, + input logic [7:0] a, b, + input logic [7:0] d[2], + output logic [7:0] x[2] + ); + always_comb begin + x[0] = a; + x[1] = b; + if (c) + x = d; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("mixed_unpacked_array_comb_writes.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let d = sim.signal("d"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 0x11u8); + io.set(b, 0x22u8); + io.set(d, 0x4433u16); + }) + .unwrap(); + assert_eq!(sim.get(x), 0x2211u16.into()); + sim.modify(|io| io.set(c, 1u8)).unwrap(); + assert_eq!(sim.get(x), 0x4433u16.into()); +} + +#[test] +fn merges_exhaustive_writes_across_different_slice_partitions() { + let source = r#" + module Top( + input logic c, + input logic [1:0] a, + input logic b, d, + output logic [1:0] x + ); + always_comb begin + if (c) + x[1:0] = a; + else begin + x[1] = b; + x[0] = d; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("exhaustive_slice_partitions.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let d = sim.signal("d"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, 0u8); + io.set(a, 0u8); + io.set(b, 1u8); + io.set(d, 0u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 2u8.into()); + sim.modify(|io| { + io.set(c, 1u8); + io.set(a, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); +} + +#[test] +fn merges_exhaustive_slice_partitions_within_a_wider_vector() { + let source = r#" + module Top( + input logic c, + input logic [1:0] a, + input logic b, d, + output logic [3:0] x + ); + always_comb begin + if (c) + x[1:0] = a; + else begin + x[1] = b; + x[0] = d; + end + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("partial_exhaustive_slice_partitions.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let d = sim.signal("d"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(c, false); + io.set(a, 0u8); + io.set(b, true); + io.set(d, false); + }) + .unwrap(); + assert_eq!(sim.get(x), 2u8.into()); + sim.modify(|io| { + io.set(c, true); + io.set(a, 1u8); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); +} + +#[test] +fn rejects_incomplete_slice_partitions_within_a_wider_vector() { + let error = cranelift_build_error( + r#" + module Top(input logic c, input logic [1:0] a, input logic b, output logic [3:0] x); + always_comb begin + if (c) + x[1:0] = a; + else + x[0] = b; + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn does_not_treat_wildcard_equality_as_inherently_two_state() { + let error = four_state_cranelift_build_error( + r#" + module Top(input logic a, output logic y); always_comb begin - y = a; - y = b; + case (a ==? 1'b0) + 1'b0: y = 1'b0; + 1'b1: y = 1'b1; + endcase end endmodule - "#; - let mut sim = Simulator::from_sv_sources(vec![(source, Path::new("comb_order.sv"))], "Top") - .build_cranelift() - .unwrap(); - let a = sim.signal("a"); - let b = sim.signal("b"); - let y = sim.signal("y"); - sim.modify(|io| { - io.set(a, 1u8); - io.set(b, 0u8); - }) - .unwrap(); - assert_eq!(sim.get(y), 0u8.into()); + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); } #[test] -fn merges_overlapping_writes_inside_always_comb() { - let source = r#" - module Top(input logic a, output logic [7:0] y); +fn rejects_reads_before_a_later_definite_fallback_write() { + let error = cranelift_build_error( + r#" + module Top( + input logic c, d, a, b, e, + output logic x, y + ); always_comb begin - y = 8'h00; - y[0] = a; + y = 1'b0; + if (c) + x = a; + else begin + if (d) + x = b; + y = x; + x = e; + end end endmodule - "#; - let mut sim = Simulator::from_sv_sources(vec![(source, Path::new("comb_overlap.sv"))], "Top") - .build_cranelift() - .unwrap(); - let a = sim.signal("a"); - let y = sim.signal("y"); - sim.modify(|io| io.set(a, 1u8)).unwrap(); - assert_eq!(sim.get(y), 1u8.into()); - sim.modify(|io| io.set(a, 0u8)).unwrap(); - assert_eq!(sim.get(y), 0u8.into()); + "#, + ); + assert!( + error.contains("inside always_comb"), + "unexpected error: {error}" + ); } #[test] @@ -2489,6 +4570,33 @@ fn treats_unknown_procedural_conditions_as_false() { ); } +#[test] +fn takes_always_comb_else_branch_for_unknown_predicates() { + let source = r#" + module Top(input logic sel, output logic y); + always_comb begin + if (sel) y = 1'b1; + else y = 1'b0; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("unknown_always_comb_else.sv"))], + "Top", + ) + .four_state(true) + .build_cranelift() + .unwrap(); + let sel = sim.signal("sel"); + let y = sim.signal("y"); + sim.modify(|io| io.set_four_state(sel, BigUint::from(1u8), BigUint::from(1u8))) + .unwrap(); + assert_eq!( + sim.get_four_state(y), + (BigUint::from(0u8), BigUint::from(0u8)) + ); +} + #[test] fn takes_always_ff_else_branch_for_unknown_predicates() { let source = r#" @@ -3096,14 +5204,6 @@ fn rejects_constructs_that_are_not_yet_lowered() { endmodule "#, ), - ( - "control flow inside always_comb", - r#" - module Top(input logic s, a, b, output logic y); - always_comb if (s) y = a; else y = b; - endmodule - "#, - ), ( "unsupported statement inside always_comb", r#" @@ -3447,7 +5547,7 @@ fn rejects_constructs_that_are_not_yet_lowered() { "#, ), ( - "procedural local data declaration", + "block-local declaration inside always_comb", r#" module Top(input logic a, output logic y); always_comb begin logic tmp; tmp = a; y = tmp; end @@ -3688,14 +5788,6 @@ fn rejects_constructs_that_are_not_yet_lowered() { endmodule "#, ), - ( - "dependent repeated assignment inside always_comb", - r#" - module Top(input logic b, d, output logic a, c); - always_comb begin a = b; c = a; a = d; end - endmodule - "#, - ), ( "reduction operator in parameter expression", r#" @@ -5545,6 +7637,714 @@ fn rejects_function_writes_outside_the_inlined_scope() { ); } +#[test] +fn handles_wide_two_state_cases_without_enumerating_the_selector_domain() { + let source = r#" + module Top(input bit [31:0] s, input logic a, b, output logic y); + always_comb begin + case (s) + 32'd0: y = a; + default: y = b; + endcase + end + endmodule + "#; + let mut sim = + Simulator::from_sv_sources(vec![(source, Path::new("wide_two_state_case.sv"))], "Top") + .build_cranelift() + .unwrap(); + let s = sim.signal("s"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(s, 0u32); + io.set(a, true); + io.set(b, false); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(s, 1u32)).unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn treats_nonempty_static_for_loops_as_definite_assignments() { + let source = r#" + module Top(input logic c, a, b, output logic y); + always_comb begin + if (c) + y = a; + else + for (int i = 0; i < 1; i++) + y = b; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("definite_static_for_loop.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + sim.modify(|io| { + io.set(c, true); + io.set(a, true); + io.set(b, false); + }) + .unwrap(); + assert_eq!(sim.get(y), true.into()); + sim.modify(|io| io.set(c, false)).unwrap(); + assert_eq!(sim.get(y), false.into()); +} + +#[test] +fn rejects_unmatched_constant_cases_that_would_infer_latches() { + let error = cranelift_build_error( + r#" + module Top(input logic a, output logic y); + always_comb begin + case (1'b0) + 1'b1: y = a; + endcase + end + endmodule + "#, + ); + assert!( + error.contains("latch inference inside always_comb"), + "unexpected error: {error}" + ); +} + +#[test] +fn sign_extends_function_calls_in_conditional_assignments() { + let source = r#" + module Top(input logic c, output logic [7:0] y); + function automatic logic signed [3:0] f(); + return -1; + endfunction + always_comb begin + if (c) + y = f(); + else + y = 8'h00; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![( + source, + Path::new("signed_function_conditional_assignment.sv"), + )], + "Top", + ) + .build_cranelift() + .unwrap(); + let c = sim.signal("c"); + let y = sim.signal("y"); + sim.modify(|io| io.set(c, true)).unwrap(); + assert_eq!(sim.get(y), 0xffu8.into()); + sim.modify(|io| io.set(c, false)).unwrap(); + assert_eq!(sim.get(y), 0u8.into()); +} + +#[test] +fn coerces_function_returns_in_procedural_lvalue_indices() { + let source = r#" + module Top( + input bit [1:0] index, + input logic data, + input logic replace, + output logic [1:0] x + ); + function automatic bit idx(); + return index; + endfunction + always_comb begin + x = '0; + x[idx()] = data; + if (replace) + x = '1; + end + endmodule + "#; + let mut sim = Simulator::from_sv_sources( + vec![(source, Path::new("function_typed_lvalue_index.sv"))], + "Top", + ) + .build_cranelift() + .unwrap(); + let index = sim.signal("index"); + let data = sim.signal("data"); + let replace = sim.signal("replace"); + let x = sim.signal("x"); + sim.modify(|io| { + io.set(index, 2u8); + io.set(data, true); + io.set(replace, false); + }) + .unwrap(); + assert_eq!(sim.get(x), 1u8.into()); + sim.modify(|io| io.set(index, 1u8)).unwrap(); + assert_eq!(sim.get(x), 2u8.into()); +} + +#[test] +fn rejects_indexed_part_selects_in_comb_write_groups() { + for select in ["index +: 2", "index -: 2"] { + let source = format!( + "module Top(input int index, input logic replace, output logic [7:0] value); \ + always_comb begin value = '0; value[{select}] = 2'b11; \ + if (replace) value = '1; end endmodule" + ); + let error = cranelift_build_error(&source); + assert!(error.contains("indexed part-select"), "{error}"); + } +} + +sv_backends! { + fn preserves_use_site_dimensions_in_parameter_alias_types(sim) { + @setup { + let source = r#" + module Top #(parameter N = 2)( + output logic [7:0] p, l, filled, + output logic [15:0] signed_y, sized_y); + typedef logic [3:0] nibble_t; + typedef logic signed [3:0] signed_nibble_t; + parameter nibble_t [1:0] P = 8'hab; + localparam nibble_t [2:1] L = P; + localparam nibble_t [1:0] F = '1; + localparam signed_nibble_t [1:0] S = 8'hab; + parameter nibble_t [N-1:0] R = 16'hcdef; + always_comb begin + p = P; + l = L; + filled = F; + signed_y = S; + sized_y = R; + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("parameter_alias_use_site_dimensions.sv"))], "Top" + ).param("P", 0xcd).param("N", 4); + for (name, expected) in [("p", 0xcdu16), ("l", 0xcd), ("filled", 0xff), ("signed_y", 0xffab), ("sized_y", 0xcdef)] { + assert_eq!(sim.get(sim.signal(name)), expected.into(), "{name}"); + } + } + + fn preserves_four_state_arithmetic_case_constants(sim) { + @setup { + let source = r#" + module Top(input logic a, output logic y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11); + always_comb begin + case (1'bx + 1'b0) 1'bx: y0 = a; endcase + case (2'b1z - 4'b0001) 4'bxxxx: y1 = a; endcase + case (4'b0000 * 2'b1x) 4'bxxxx: y2 = a; endcase + case (2'b1z / 2'b01) 2'bxx: y3 = a; endcase + case (2'b1x % 2'b01) 2'bxx: y4 = a; endcase + case (-(2'b1z)) 2'bxx: y5 = a; endcase + case ((2'b11 + 2'b01) + 2'b0x) 2'bxx: y6 = a; endcase + case ((2'b1x + 2'b01) & 2'b00) 2'b00: y7 = a; endcase + case (1'bx) (1'bx + 1'b0): y8 = a; endcase + case (2'b01 / 2'b1x) 2'bxx: y9 = a; endcase + case (2'b01 % 2'b1z) 2'bxx: y10 = a; endcase + case (+(2'b1z)) 2'bxx: y11 = a; endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("four_state_arithmetic_case_constants.sv"))], "Top" + ).four_state(true); + let a = sim.signal("a"); + let outputs = (0..12).map(|index| sim.signal(&format!("y{index}"))).collect::>(); + for value in [true, false, true] { + sim.modify(|io| io.set(a, value)).unwrap(); + for &output in &outputs { + assert_eq!(sim.get(output), value.into()); + } + } + } + + fn preserves_four_state_reduction_case_constants(sim) { + @setup { + let source = r#" + module Top(input logic a, output logic y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11); + always_comb begin + case (&1'bx) 1'bx: y0 = a; endcase + case (|1'bz) 1'bx: y1 = a; endcase + case (^2'b1x) 1'bx: y2 = a; endcase + case (&3'b1z0) 1'b0: y3 = a; endcase + case (|3'b0z1) 1'b1: y4 = a; endcase + case (~&2'b1z) 1'bx: y5 = a; endcase + case (~|2'b0x) 1'bx: y6 = a; endcase + case (~^2'b1z) 1'bx: y7 = a; endcase + case (^~2'b1x) 1'bx: y8 = a; endcase + case (1'bx) (&1'bx): y9 = a; endcase + case (^'1) 1'b1: y10 = a; endcase + case (&'z) 1'bx: y11 = a; endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("four_state_reduction_case_constants.sv"))], "Top" + ).four_state(true); + let a = sim.signal("a"); + let outputs = (0..12).map(|index| sim.signal(&format!("y{index}"))).collect::>(); + for value in [true, false, true] { + sim.modify(|io| io.set(a, value)).unwrap(); + for &output in &outputs { + assert_eq!(sim.get(output), value.into()); + } + } + } + + fn preserves_use_site_dimensions_in_function_alias_types(sim) { + @setup { + let source = r#" + module Top #(parameter W = 4)(input logic [7:0] data, + output logic [7:0] constant_y, echo_y, + output logic [3:0] high_y, low_y, ascending_y, inherited_y, + output logic [15:0] signed_y, sizes); + typedef logic [3:0] nibble_t; + typedef logic signed [3:0] signed_nibble_t; + function automatic nibble_t [1:0] constant_value(); + return 8'hab; + endfunction + function automatic nibble_t [W'(2):W'(1)] echo( + input nibble_t [W'(2):W'(1)] x); + return x; + endfunction + function automatic nibble_t high(input nibble_t [2:1] x); + return x[2]; + endfunction + function automatic nibble_t low; + input nibble_t [2:1] x; + return x[1]; + endfunction + function automatic nibble_t ascending(input nibble_t [1:2] x); + return x[1]; + endfunction + function automatic nibble_t inherited(input nibble_t [2:1] x, z); + return z[2]; + endfunction + function automatic signed_nibble_t [1:0] signed_echo( + input signed_nibble_t [1:0] x); + return x; + endfunction + localparam BITS = $bits(constant_value())'(16'hffff); + localparam SIZE = $size(constant_value())'(8'hff); + always_comb begin + constant_y = constant_value(); + echo_y = echo(data); + high_y = high(data); + low_y = low(data); + ascending_y = ascending(data); + inherited_y = inherited('0, data); + signed_y = signed_echo(data); + sizes = {BITS, SIZE}; + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("function_alias_use_site_dimensions.sv"))], "Top" + ).four_state(true); + let data = sim.signal("data"); + let constant_y = sim.signal("constant_y"); + let echo_y = sim.signal("echo_y"); + let high_y = sim.signal("high_y"); + let low_y = sim.signal("low_y"); + let ascending_y = sim.signal("ascending_y"); + let inherited_y = sim.signal("inherited_y"); + let signed_y = sim.signal("signed_y"); + let sizes = sim.signal("sizes"); + for value in [0xabu8, 0x80, 0x12, 0xff, 0] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(constant_y), 0xabu8.into()); + assert_eq!(sim.get(echo_y), value.into()); + assert_eq!(sim.get(high_y), (value >> 4).into()); + assert_eq!(sim.get(low_y), (value & 0xf).into()); + assert_eq!(sim.get(ascending_y), (value >> 4).into()); + assert_eq!(sim.get(inherited_y), (value >> 4).into()); + assert_eq!(sim.get(signed_y), (value as i8 as i16 as u16).into()); + assert_eq!(sim.get(sizes), 0x3ffu16.into()); + } + } + + fn preserves_four_state_relational_case_selectors(sim) { + @setup { + let source = r#" + module Top(input logic a, output logic y0, y1, y2, y3); + always_comb begin + case (1'bx < 1'b1) 1'bx: y0 = a; endcase + case (1'bz <= 1'b0) 1'bx: y1 = a; endcase + case (2'b1x > 2'b00) 1'bx: y2 = a; endcase + case (2'b0z >= 2'b10) 1'bx: y3 = a; endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("four_state_relational_case_selectors.sv"))], "Top" + ).four_state(true); + let a = sim.signal("a"); + let outputs = ["y0", "y1", "y2", "y3"].map(|name| sim.signal(name)); + for value in [true, false, true] { + sim.modify(|io| io.set(a, value)).unwrap(); + for output in outputs { + assert_eq!(sim.get(output), value.into()); + } + } + } + + fn folds_compound_four_state_case_labels(sim) { + @setup { + let source = r#" + module Top(input logic s, a, b, + output logic constant_y, concat_y, function_y, dynamic_y); + function automatic logic label(); return 1'bx | 1'b0; endfunction + always_comb begin + case (1'bx) (1'bx | 1'b0): constant_y = a; endcase + case (2'bxz) {1'bx, 1'bz}: concat_y = a; endcase + case (1'bx) label(): function_y = a; endcase + case (s) + (1'b0 & 1'b1): dynamic_y = a; + (1'b1 | 1'b0): dynamic_y = b; + (1'bx | 1'b0): dynamic_y = a ^ b; + (1'bx ? 1'bz : 1'bz): dynamic_y = ~a; + endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("compound_four_state_case_labels.sv"))], "Top" + ).four_state(true); + let s = sim.signal("s"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let constant_outputs = ["constant_y", "concat_y", "function_y"].map(|name| sim.signal(name)); + let dynamic_y = sim.signal("dynamic_y"); + for inputs in 0u8..4 { + let a_value = inputs & 1 != 0; + let b_value = inputs & 2 != 0; + for (value, mask, expected) in [ + (0u8, 0u8, a_value), + (1, 0, b_value), + (1, 1, a_value ^ b_value), + (0, 1, !a_value), + ] { + sim.modify(|io| { + io.set(a, a_value); + io.set(b, b_value); + io.set_four_state(s, BigUint::from(value), BigUint::from(mask)); + }).unwrap(); + for output in constant_outputs { + assert_eq!(sim.get(output), a_value.into()); + } + assert_eq!(sim.get(dynamic_y), expected.into()); + } + } + } + + fn preserves_four_state_equality_case_selectors(sim) { + @setup { + let source = r#" + module Top(input logic a, + output logic y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11); + always_comb begin + case (1'bx == 1'bx) 1'bx: y0 = a; endcase + case (1'bz != 1'b0) 1'bx: y1 = a; endcase + case (2'b0x == 2'b1x) 1'b0: y2 = a; endcase + case (2'b0z != 2'b1x) 1'b1: y3 = a; endcase + case (!(1'bx == 1'b0)) 1'bx: y4 = a; endcase + case ((1'bx != 1'bz) && 1'b1) 1'bx: y5 = a; endcase + case ((1'bx && 1'b1) == 1'bx) 1'bx: y6 = a; endcase + case ((1'bx == 1'bx) ? 1'b0 : 1'b1) 1'bx: y7 = a; endcase + case ((1'bx == 1'bx) ? 1'bz : 1'bz) 1'bz: y8 = a; endcase + case (1'sbx == 2'b1x) 1'b0: y9 = a; endcase + case (1'sbx != 2'sb1x) 1'bx: y10 = a; endcase + case (8'hff == '1) 1'b1: y11 = a; endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("four_state_equality_case_selectors.sv"))], "Top" + ).four_state(true); + let a = sim.signal("a"); + let outputs = (0..12).map(|index| sim.signal(&format!("y{index}"))).collect::>(); + for value in [true, false, true] { + sim.modify(|io| io.set(a, value)).unwrap(); + for output in &outputs { + assert_eq!(sim.get(*output), value.into()); + } + } + } + + fn preserves_constant_case_selector_context(sim) { + @setup { + let source = r#" + module Top(input logic a, output logic y0, y1, y2, y3, y4, y5); + always_comb begin + case (1'bx && 1'b1) 1'bx: y0 = a; endcase + case (1'b0 || 1'bz) 1'bx: y1 = a; endcase + case (1'b1 ? 1'sb1 : 2'sb00) 2'b11: y2 = a; endcase + case (1'b0 ? 2'sb00 : 1'sb1) 2'b11: y3 = a; endcase + case (1'b1 ? 1'sb1 : 2'b00) 2'b01: y4 = a; endcase + case (1'b1 ? 1'sbx : 2'sb00) 2'bxx: y5 = a; endcase + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("constant_case_context.sv"))], "Top" + ).four_state(true); + let a = sim.signal("a"); + let outputs = ["y0", "y1", "y2", "y3", "y4", "y5"].map(|name| sim.signal(name)); + for value in [true, false, true] { + sim.modify(|io| io.set(a, value)).unwrap(); + for output in outputs { + assert_eq!(sim.get(output), value.into()); + } + } + } + + fn preserves_size_cast_dimensions_in_declarations_and_selections(sim) { + @setup { + let source = r#" + module Top(input logic [7:0] data, + output logic [$bits(f())'(7):0] y, output logic [7:0] sizes); + function automatic logic [7:0] f(); return '0; endfunction + logic [$size(f())'(7):0] value; + logic [1:0][3:0] a[2]; + localparam P = $size(a[0])'(8'hff); + localparam Q = $size(a[0][0])'(8'hff); + always_comb begin + value = data; + y = value; + sizes = {P, Q, 2'b00}; + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("size_cast_dimensions.sv"))], "Top" + ); + let data = sim.signal("data"); + let y = sim.signal("y"); + let sizes = sim.signal("sizes"); + for value in [0x80u8, 1, 0x55, 0xff, 0] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(y), value.into()); + assert_eq!(sim.get(sizes), 0xfcu8.into()); + } + } + + fn resolves_generate_local_alias_casts_for_all_processes(sim) { + @setup { + let source = r#" + module Buffer(input logic a, output logic y); + assign y = a; + endmodule + module Top(input logic clk, data, output logic comb_y, ff_y); + typedef logic [1:0] select_t; + if (1) begin : enabled + localparam S = select_t'(4); + localparam select_t WIDTH = 1; + logic [WIDTH-1:0] connected; + Buffer u(.a(data), .y(connected)); + if (S) begin : disabled + assign comb_y = 1'b0; + always_ff @(posedge clk) ff_y <= 1'b0; + end else begin : selected + always_comb comb_y = connected; + always_ff @(posedge clk) ff_y <= connected; + end + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("generate_local_alias_casts.sv"))], "Top" + ); + let data = sim.signal("data"); + let comb_y = sim.signal("comb_y"); + let ff_y = sim.signal("ff_y"); + let clk = sim.event("clk"); + for value in [true, false, true] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(comb_y), value.into()); + sim.tick(clk).unwrap(); + assert_eq!(sim.get(ff_y), value.into()); + } + } + + fn covers_single_bit_bitwise_complementary_guards(sim) { + @setup { + let source = r#" + module Top(input bit s, input logic outer, a, b, output logic y); + always_comb if (outer) begin + if (s) y = a; + if (~s) y = b; + end else y = a; + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("bitwise_complementary_guards.sv"))], "Top" + ).four_state(true); + let s = sim.signal("s"); + let outer = sim.signal("outer"); + let a = sim.signal("a"); + let b = sim.signal("b"); + let y = sim.signal("y"); + for inputs in 0u8..16 { + sim.modify(|io| { + io.set(s, inputs & 1 != 0); + io.set(outer, inputs & 2 != 0); + io.set(a, inputs & 4 != 0); + io.set(b, inputs & 8 != 0); + }).unwrap(); + let expected = if inputs & 2 == 0 || inputs & 1 != 0 { + inputs & 4 != 0 + } else { + inputs & 8 != 0 + }; + assert_eq!(sim.get(y), expected.into()); + } + } + + fn normalizes_function_parameter_cast_dimensions(sim) { + @setup { + let source = r#" + module Top #(parameter W = 5)( + input logic [7:0] data, + output logic [5:0] y + ); + typedef logic [3:0] index_t; + typedef logic [W'(8):W'(1)] byte_t; + function automatic logic ansi(input logic [W'(8):W'(1)] x); + return x[7]; + endfunction + function automatic logic nonansi; + input logic [W'(8):W'(1)] x; + return x[7]; + endfunction + function automatic logic ascending(input logic [W'(1):W'(8)] x); + return x[2]; + endfunction + function automatic logic inherited(input logic [W'(8):W'(1)] x, z); + return z[7]; + endfunction + function automatic logic typecast(input logic [index_t'(8):index_t'(1)] x); + return x[7]; + endfunction + function automatic logic alias_range(input byte_t x); + return x[7]; + endfunction + always_comb begin + y[0] = ansi(data); + y[1] = nonansi(data); + y[2] = ascending(data); + y[3] = inherited('0, data); + y[4] = typecast(data); + y[5] = alias_range(data); + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("function_parameter_cast_dimensions.sv"))], "Top" + ); + let data = sim.signal("data"); + let y = sim.signal("y"); + for value in [0x40u8, 0x80, 0, 0xff] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(y), if value & 0x40 != 0 { 0x3fu8 } else { 0 }.into()); + } + } + + fn lowers_parameter_casts_in_conditional_generate(sim) { + @setup { + let source = r#" + module Buffer(input logic a, output logic y); + assign y = a; + endmodule + module Top #(parameter W = 2)( + input logic clk, data, + output logic comb_y, ff_y + ); + typedef logic [W-1:0] select_t; + if (W'(1)) begin : enabled + logic connected; + Buffer u(.a(data), .y(connected)); + if (select_t'(4)) begin : disabled + assign comb_y = 1'b0; + always_ff @(posedge clk) ff_y <= 1'b0; + end else begin : selected + always_comb comb_y = connected; + always_ff @(posedge clk) ff_y <= connected; + end + end else begin : disabled + assign comb_y = 1'b0; + always_ff @(posedge clk) ff_y <= 1'b0; + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("conditional_generate_casts.sv"))], "Top" + ); + let data = sim.signal("data"); + let comb_y = sim.signal("comb_y"); + let ff_y = sim.signal("ff_y"); + let clk = sim.event("clk"); + for value in [true, false, true] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(comb_y), value.into()); + sim.tick(clk).unwrap(); + assert_eq!(sim.get(ff_y), value.into()); + } + } + + fn lowers_parameter_casts_in_loop_generate(sim) { + @setup { + let source = r#" + module Top #(parameter W = 3)( + input logic [3:0] data, + output logic [3:0] y + ); + typedef logic [W-1:0] index_t; + for (genvar i = W'(8); i < index_t'(4); i += W'(1)) begin : bits + if (W'(1)) begin : enabled + assign y[i] = data[i]; + end + end + endmodule + "#; + } + @build Simulator::from_sv_sources( + vec![(source, Path::new("loop_generate_casts.sv"))], "Top" + ); + let data = sim.signal("data"); + let y = sim.signal("y"); + for value in [1u8, 2, 4, 8, 0xf, 0] { + sim.modify(|io| io.set(data, value)).unwrap(); + assert_eq!(sim.get(y), value.into()); + } + } +} + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] #[test] fn collapses_unknown_initializers_in_two_state_native_images() {