From 95045e14af28a51d469f0ee3bbe71a75090097a4 Mon Sep 17 00:00:00 2001 From: tignear Date: Sun, 23 Aug 2026 16:56:03 +0900 Subject: [PATCH 01/55] feat(sv-frontend): lower enums and always_comb control flow Enable the enum_type sv tests by closing three frontend gaps: - fold constant casts of any sized literal, not just zero - resolve $bits and $size of a data type as a cast target width - collect typedef enum member constants as module-level literals - lower if/case inside always_comb into guarded mux chains with latch detection instead of rejecting all comb control flow --- crates/celox-sv-analyzer/src/ast.rs | 469 +++++++++++++----- crates/celox/tests/enum_type.rs | 3 - .../systemverilog/review_regressions.rs | 8 - 3 files changed, 354 insertions(+), 126 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index b56391538..91a123e2b 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -220,7 +220,10 @@ 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_member_constants_from_module_node(node.clone(), syntax_tree)? { + parameter_values.entry(name).or_insert(value); + } let mut expression_signedness = ports .iter() .map(|port| (port.name().to_string(), port.r#type().is_signed())) @@ -608,6 +611,9 @@ fn cast_target_type( expr_type_from_type(type_aliases.get(&name)?, const_env) } sv_parser::CastingType::ConstantPrimary(primary) => { + if let Some(r#type) = size_system_function_expr_type(primary, syntax_tree, const_env) { + 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); @@ -622,6 +628,33 @@ fn cast_target_type( } } +fn size_system_function_expr_type( + primary: &sv_parser::ConstantPrimary, + syntax_tree: &SyntaxTree, + const_env: &HashMap, +) -> Option { + let sv_parser::ConstantPrimary::ConstantFunctionCall(call) = primary else { + return None; + }; + let sv_parser::SubroutineCall::SystemTfCall(system_call) = &call.nodes.0.nodes.0 else { + return None; + }; + let sv_parser::SystemTfCall::ArgDataType(call) = &**system_call else { + return None; + }; + let name = syntax_tree.get_str(&call.nodes.0.nodes.0)?; + if name != "$bits" && name != "$size" { + return None; + } + let data_type = unwrap_node!( + RefNode::SystemTfCallArgDataType(call), + DataTypeVector, + DataTypeAtom + )?; + let r#type = type_from_ref_node(data_type.clone(), syntax_tree)?; + expr_type_from_type(&r#type, const_env) +} + fn expr_type_from_type(r#type: &Type, const_env: &HashMap) -> Option { if !r#type.unpacked_ranges().is_empty() { return None; @@ -655,7 +688,7 @@ fn constant_cast_is_supported( const_env: &HashMap, type_aliases: &HashMap, ) -> bool { - constant_cast_zero_type(cast, syntax_tree, const_env, type_aliases).is_some() + constant_cast_const_expr(cast, syntax_tree, const_env, type_aliases).is_some() } fn cast_zero_type( @@ -684,12 +717,12 @@ fn cast_zero_type( }) } -fn constant_cast_zero_type( +fn constant_cast_const_expr( cast: &sv_parser::ConstantCast, syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, -) -> Option { +) -> Option { let ConstExpr::Literal(literal) = const_expr_from_ref_node( RefNode::ConstantExpression(&cast.nodes.2.nodes.1), syntax_tree, @@ -698,27 +731,23 @@ fn constant_cast_zero_type( return None; }; let literal = typecheck::parse_integral_literal(&literal)?; - if literal.value != 0u8.into() || literal.mask != 0u8.into() { + if 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 + let value = if literal.value.bits() > target_type.width as u64 { + literal.value + & ((num_bigint::BigUint::from(1usize) << target_type.width) + - num_bigint::BigUint::from(1usize)) } else { - target_type.signed + literal.value.clone() }; - Some(ExprType { - signed, - ..target_type - }) -} - -fn typed_zero_literal(r#type: ExprType) -> String { - format!( - "{}'{}d0", - r#type.width, - if r#type.signed { "s" } else { "" } - ) + Some(ConstExpr::Literal(format!( + "{}'{}d{}", + target_type.width, + if target_type.signed { "s" } else { "" }, + value + ))) } fn for_loop_variable_lvalue_name( @@ -844,18 +873,6 @@ fn reject_silently_ignored_constructs( )); } 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() @@ -3061,6 +3078,50 @@ fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 } } +/// Collect enum member constants declared by module-level `typedef enum` +/// declarations. Members must carry explicit values, matching what Veryl +/// emits; the values become literal expressions usable anywhere in the +/// module body. +fn enum_member_constants_from_module_node( + node: RefNode<'_>, + syntax_tree: &SyntaxTree, +) -> Result, AnalyzerError> { + let mut constants = HashMap::default(); + for item in module_non_port_items(node) { + let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { + continue; + }; + let sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) = declaration else { + 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; + }; + 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()) + })?; + 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(RefNode::ConstantExpression(value), syntax_tree) + .map(const_expr_to_expr) + .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` value")))?; + constants.insert(name, value); + } + } + Ok(constants) +} + fn parameter_value_env( parameters: &[Parameter], const_env: &HashMap, @@ -6964,23 +7025,199 @@ fn comb_process_from_always_construct( return Ok(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 mut guarded_assignments = Vec::new(); + conditional_assignments_from_statement( + &always.nodes.1, + None, + true, + syntax_tree, + &packed_dimensions.const_env, + packed_dimensions, + &mut guarded_assignments, + )?; + let assignments = comb_assignments_from_guarded(guarded_assignments)?; Ok((!assignments.is_empty()) .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) } +/// 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( + guarded: Vec, +) -> Result, AnalyzerError> { + 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(); + match targets.iter().position(|existing| existing == target) { + Some(group) => groups[group].push(index), + None => { + targets.push(target.clone()); + groups.push(vec![index]); + } + } + } + // Merged groups land on the slot of their last write so relative + // statement ordering across different 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()); + } + continue; + } + let mut current: Option = None; + for index in indices.iter().rev() { + let write = &guarded[*index]; + let value = write.assignment().rhs().clone(); + current = Some(match write.condition() { + None => value, + Some(condition) => { + // A conditional write with no later unconditional + // fallback would keep the previous value, which infers + // a latch; represent that tentatively with a + // self-reference and reject it below. + let otherwise = current + .unwrap_or_else(|| lvalue_self_reference(write.assignment().lhs_value())); + Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(otherwise), + } + } + }); + } + let rhs = current.expect("guarded writes exist"); + if expr_references_signal(&rhs, write_target_base_name(&target)) { + return Err(AnalyzerError::Unsupported( + "latch inference inside always_comb".to_string(), + )); + } + slots[*indices.last().expect("group is non-empty")] = Some(Assignment::new(target, rhs)); + } + Ok(slots.into_iter().flatten().collect()) +} + +fn lvalue_self_reference(target: &LValue) -> Expr { + match target { + LValue::Ident(name) => Expr::Ident(name.clone()), + LValue::Select { + name, + msb, + lsb, + signed, + .. + } => Expr::Select { + expr: Box::new(Expr::Ident(name.clone())), + msb: msb.clone(), + lsb: lsb.clone(), + signed: *signed, + }, + } +} + +fn write_target_base_name(target: &LValue) -> Option<&str> { + match target { + LValue::Ident(name) => Some(name), + LValue::Select { name, .. } => Some(name), + } +} + +fn expr_references_signal(expr: &Expr, signal: Option<&str>) -> bool { + let Some(signal) = signal else { + return false; + }; + match expr { + Expr::Ident(name) => name == signal, + Expr::Literal(_) => false, + Expr::Select { expr, msb, lsb, .. } => { + expr_references_signal(expr, Some(signal)) + || const_expr_references_signal(msb, signal) + || const_expr_references_signal(lsb, signal) + } + Expr::Concat(parts) => parts + .iter() + .any(|part| expr_references_signal(part, Some(signal))), + Expr::RepeatConcat { parts, .. } => parts + .iter() + .any(|part| expr_references_signal(part, Some(signal))), + Expr::Resize { expr, .. } => expr_references_signal(expr, Some(signal)), + Expr::Call { args, .. } => args + .iter() + .any(|arg| expr_references_signal(arg, Some(signal))), + Expr::Unary { expr, .. } => expr_references_signal(expr, Some(signal)), + Expr::Binary { left, right, .. } => { + expr_references_signal(left, Some(signal)) + || expr_references_signal(right, Some(signal)) + } + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_references_signal(condition, Some(signal)) + || expr_references_signal(then_expr, Some(signal)) + || expr_references_signal(else_expr, Some(signal)) + } + } +} + +fn const_expr_references_signal(expr: &ConstExpr, signal: &str) -> bool { + match expr { + ConstExpr::Ident(name) => name == signal, + ConstExpr::Literal(_) => false, + ConstExpr::Select { expr, bit } => { + const_expr_references_signal(expr, signal) || const_expr_references_signal(bit, signal) + } + ConstExpr::Function { args, .. } => args + .iter() + .any(|arg| const_expr_references_signal(arg, signal)), + ConstExpr::Unary { expr, .. } => const_expr_references_signal(expr, signal), + ConstExpr::Binary { left, right, .. } => { + const_expr_references_signal(left, signal) + || const_expr_references_signal(right, signal) + } + ConstExpr::Mux { + condition, + then_expr, + else_expr, + } => { + const_expr_references_signal(condition, signal) + || const_expr_references_signal(then_expr, signal) + || const_expr_references_signal(else_expr, signal) + } + } +} + fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), AnalyzerError> { match &stmt.nodes.2 { - sv_parser::StatementItem::BlockingAssignment(_) => Ok(()), + sv_parser::StatementItem::BlockingAssignment(_) + | sv_parser::StatementItem::ConditionalStatement(_) => 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(_)) { + Ok(()) + } else { + Err(AnalyzerError::Unsupported( + "casez or casex inside always_comb".to_string(), + )) + } + } sv_parser::StatementItem::SeqBlock(block) => { for stmt in &block.nodes.3 { if let sv_parser::StatementOrNull::Statement(stmt) = stmt { @@ -7216,6 +7453,7 @@ fn ff_process_from_always_construct( conditional_assignments_from_statement_or_null( body, None, + false, syntax_tree, const_env, packed_dimensions, @@ -7306,6 +7544,7 @@ fn ff_events_from_event_expression( fn conditional_assignments_from_statement_or_null( stmt: &sv_parser::StatementOrNull, condition: Option, + exhaustive_fallback: bool, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, @@ -7315,6 +7554,7 @@ fn conditional_assignments_from_statement_or_null( conditional_assignments_from_statement( stmt, condition, + exhaustive_fallback, syntax_tree, const_env, packed_dimensions, @@ -7327,12 +7567,56 @@ fn conditional_assignments_from_statement_or_null( fn conditional_assignments_from_statement( stmt: &sv_parser::Statement, condition: Option, + exhaustive_fallback: 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(), + )); + }; + 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) @@ -7357,6 +7641,7 @@ fn conditional_assignments_from_statement( conditional_assignments_from_statement_or_null( stmt, condition.clone(), + exhaustive_fallback, syntax_tree, const_env, packed_dimensions, @@ -7368,6 +7653,7 @@ fn conditional_assignments_from_statement( conditional_assignments_from_conditional_statement( stmt, condition, + exhaustive_fallback, syntax_tree, const_env, packed_dimensions, @@ -7378,6 +7664,7 @@ fn conditional_assignments_from_statement( conditional_assignments_from_case_statement( stmt, condition, + exhaustive_fallback, syntax_tree, const_env, packed_dimensions, @@ -7411,6 +7698,7 @@ fn conditional_assignments_from_statement( conditional_assignments_from_statement_or_null( body, condition.clone(), + exhaustive_fallback, syntax_tree, &loop_const_env, &loop_packed_dimensions, @@ -7441,6 +7729,7 @@ fn conditional_assignments_from_statement( fn conditional_assignments_from_conditional_statement( stmt: &sv_parser::ConditionalStatement, parent_condition: Option, + exhaustive_fallback: bool, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, @@ -7456,6 +7745,7 @@ fn conditional_assignments_from_conditional_statement( conditional_assignments_from_statement_or_null( &stmt.nodes.3, then_condition, + false, syntax_tree, const_env, packed_dimensions, @@ -7475,6 +7765,7 @@ fn conditional_assignments_from_conditional_statement( conditional_assignments_from_statement_or_null( branch, condition, + false, syntax_tree, const_env, packed_dimensions, @@ -7484,10 +7775,15 @@ fn conditional_assignments_from_conditional_statement( } if let Some((_, branch)) = &stmt.nodes.5 { - let condition = combine_expr_condition_terms(parent_condition, prior_false); + let condition = if exhaustive_fallback { + None + } else { + combine_expr_condition_terms(parent_condition, prior_false) + }; conditional_assignments_from_statement_or_null( branch, condition, + false, syntax_tree, const_env, packed_dimensions, @@ -7510,6 +7806,7 @@ fn procedural_false_condition(condition: Expr) -> Expr { fn conditional_assignments_from_case_statement( stmt: &sv_parser::CaseStatement, parent_condition: Option, + exhaustive_fallback: bool, syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, @@ -7569,6 +7866,7 @@ fn conditional_assignments_from_case_statement( conditional_assignments_from_statement_or_null( branch, condition, + exhaustive_fallback, syntax_tree, const_env, packed_dimensions, @@ -7581,10 +7879,15 @@ fn conditional_assignments_from_case_statement( } if let Some(branch) = default_branch { - let condition = combine_expr_condition_terms(parent_condition, prior_false); + let condition = if exhaustive_fallback { + None + } else { + combine_expr_condition_terms(parent_condition, prior_false) + }; conditional_assignments_from_statement_or_null( branch, condition, + false, syntax_tree, const_env, packed_dimensions, @@ -7628,72 +7931,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, @@ -9660,10 +9897,12 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti .map(ConstExpr::Ident) }) } - 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))) - } + sv_parser::ConstantPrimary::ConstantCast(cast) => constant_cast_const_expr( + cast, + syntax_tree, + &HashMap::default(), + &HashMap::default(), + ), 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) 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/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 9b1b533c5..b8a82ba7b 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3096,14 +3096,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#" From 4f0e42cf6690b75f767cea5be289cbc363f9e9bd Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 00:40:42 +0900 Subject: [PATCH 02/55] fix(sv-frontend): address enum lowering review feedback --- crates/celox-sv-analyzer/src/ast.rs | 238 ++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 209 +++++++++++++++ .../frontends/systemverilog/always_comb.rs | 43 ++++ 3 files changed, 447 insertions(+), 43 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 91a123e2b..7d2e4d36b 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -163,7 +163,12 @@ impl Module { if name == override_module_name { apply_parameter_overrides(&mut parameters, parameter_overrides)?; } - let const_env = const_env_from_parameters(¶meters); + let mut const_env = const_env_from_parameters(¶meters); + let enum_constants = + enum_member_constants_from_module_node(node.clone(), syntax_tree, &const_env)?; + for (name, value) in &enum_constants.numbers { + const_env.entry(name.clone()).or_insert(*value); + } 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)?; @@ -221,8 +226,10 @@ impl Module { } reject_unsupported_multidimensional_packed_bounds(&ports, &signals, &const_env)?; let mut parameter_values = parameter_value_env(¶meters, &const_env); - for (name, value) in enum_member_constants_from_module_node(node.clone(), syntax_tree)? { - parameter_values.entry(name).or_insert(value); + for (name, value) in &enum_constants.exprs { + parameter_values + .entry(name.clone()) + .or_insert_with(|| value.clone()); } let mut expression_signedness = ports .iter() @@ -643,7 +650,9 @@ fn size_system_function_expr_type( return None; }; let name = syntax_tree.get_str(&call.nodes.0.nodes.0)?; - if name != "$bits" && name != "$size" { + // $bits covers every packed dimension; $size covers only the first. + let first_dimension_only = name == "$size"; + if name != "$bits" && !first_dimension_only { return None; } let data_type = unwrap_node!( @@ -652,7 +661,17 @@ fn size_system_function_expr_type( DataTypeAtom )?; let r#type = type_from_ref_node(data_type.clone(), syntax_tree)?; - expr_type_from_type(&r#type, const_env) + if !first_dimension_only { + return expr_type_from_type(&r#type, const_env); + } + let range = r#type.packed_ranges().first()?; + let left = eval_ast_const_expr(range.left(), const_env)?; + let right = eval_ast_const_expr(range.right(), const_env)?; + let width = usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?; + Some(ExprType { + width: width.max(1), + signed: r#type.is_signed(), + }) } fn expr_type_from_type(r#type: &Type, const_env: &HashMap) -> Option { @@ -735,17 +754,35 @@ fn constant_cast_const_expr( return None; } let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; - let value = if literal.value.bits() > target_type.width as u64 { - literal.value - & ((num_bigint::BigUint::from(1usize) << target_type.width) - - num_bigint::BigUint::from(1usize)) + // A size cast keeps the source expression's signedness when the target + // is described by a constant primary; a type cast takes the target's. + let signed = if matches!(cast.nodes.0, sv_parser::CastingType::ConstantPrimary(_)) { + literal.signed } else { - literal.value.clone() + target_type.signed }; + let mut value = literal.value.clone(); + // Widen negative two's-complement sources with sign extension before + // applying the truncating cast. + if literal.signed + && literal.width > 0 + && literal.width < target_type.width + && (literal.value >> (literal.width - 1)) & num_bigint::BigUint::from(1usize) + == num_bigint::BigUint::from(1usize) + { + value |= ((num_bigint::BigUint::from(1usize) << target_type.width) + - num_bigint::BigUint::from(1usize)) + ^ ((num_bigint::BigUint::from(1usize) << literal.width) + - num_bigint::BigUint::from(1usize)); + } + if value.bits() > target_type.width as u64 { + value &= (num_bigint::BigUint::from(1usize) << target_type.width) + - num_bigint::BigUint::from(1usize); + } Some(ConstExpr::Literal(format!( "{}'{}d{}", target_type.width, - if target_type.signed { "s" } else { "" }, + if signed { "s" } else { "" }, value ))) } @@ -3077,16 +3114,27 @@ fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 bits as i128 } } +/// 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, +} /// Collect enum member constants declared by module-level `typedef enum` /// declarations. Members must carry explicit values, matching what Veryl -/// emits; the values become literal expressions usable anywhere in the -/// module body. +/// 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, -) -> Result, AnalyzerError> { - let mut constants = HashMap::default(); + base_const_env: &HashMap, +) -> Result { + let mut constants = EnumMemberConstants::default(); + let mut eval_env = base_const_env.clone(); for item in module_non_port_items(node) { let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { continue; @@ -3114,9 +3162,19 @@ fn enum_member_constants_from_module_node( ))); }; let value = const_expr_from_ref_node(RefNode::ConstantExpression(value), syntax_tree) - .map(const_expr_to_expr) - .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` value")))?; - constants.insert(name, value); + .ok_or_else(|| { + AnalyzerError::Unsupported(format!("enum member `{name}` value")) + })?; + if let Some(number) = eval_ast_const_expr(&value, &eval_env) { + constants.numbers.insert(name.clone(), number); + eval_env.insert(name.clone(), number); + } + let expr = substitute_expr_constants_with_parameter_literals( + const_expr_to_expr(value), + base_const_env, + &constants.exprs, + ); + constants.exprs.insert(name, expr); } } Ok(constants) @@ -7049,7 +7107,7 @@ fn comb_process_from_always_construct( /// conditional write with no later unconditional fallback would keep the /// previous value, which infers a latch, and is rejected. fn comb_assignments_from_guarded( - guarded: Vec, + mut guarded: Vec, ) -> Result, AnalyzerError> { let mut targets: Vec = Vec::new(); let mut groups: Vec> = Vec::new(); @@ -7064,7 +7122,9 @@ fn comb_assignments_from_guarded( } } // Merged groups land on the slot of their last write so relative - // statement ordering across different targets is preserved. + // statement ordering across different targets is preserved. Substitute + // values established by earlier writes into intervening reads before the + // group is moved, preserving blocking-assignment statement semantics. 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) { @@ -7077,38 +7137,122 @@ fn comb_assignments_from_guarded( } continue; } + let last = *indices.last().expect("group is non-empty"); + let name = write_target_base_name(&target); + if let LValue::Ident(name) = &target { + substitute_intermediate_comb_value_reads(&mut guarded, &indices, name)?; + } let mut current: Option = None; - for index in indices.iter().rev() { - let write = &guarded[*index]; - let value = write.assignment().rhs().clone(); - current = Some(match write.condition() { - None => value, - Some(condition) => { - // A conditional write with no later unconditional - // fallback would keep the previous value, which infers - // a latch; represent that tentatively with a - // self-reference and reject it below. - let otherwise = current - .unwrap_or_else(|| lvalue_self_reference(write.assignment().lhs_value())); - Expr::Mux { + if guarded[indices[0]].condition().is_none() { + // A leading unconditional write establishes the procedural + // default. Later guarded writes override it, just as blocking + // assignments do in source order. + for index in &indices { + let write = &guarded[*index]; + let value = write.assignment().rhs().clone(); + current = Some(match write.condition() { + None => value, + Some(condition) => Expr::Mux { condition: Box::new(condition.clone()), then_expr: Box::new(value), - else_expr: Box::new(otherwise), + else_expr: Box::new(current.expect("leading default establishes a value")), + }, + }); + } + } else { + // Branches are emitted in priority order, with an exhaustive + // else/default represented as the final unconditional fallback. + for index in indices.iter().rev() { + let write = &guarded[*index]; + let value = write.assignment().rhs().clone(); + current = Some(match write.condition() { + None => value, + Some(condition) => { + // A conditional write with no later unconditional + // fallback would keep the previous value, which + // infers a latch; represent that tentatively with a + // self-reference and reject it below. + let otherwise = current.unwrap_or_else(|| { + lvalue_self_reference(write.assignment().lhs_value()) + }); + Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(otherwise), + } } - } - }); + }); + } } let rhs = current.expect("guarded writes exist"); - if expr_references_signal(&rhs, write_target_base_name(&target)) { + if expr_references_signal(&rhs, name) { return Err(AnalyzerError::Unsupported( "latch inference inside always_comb".to_string(), )); } - slots[*indices.last().expect("group is non-empty")] = Some(Assignment::new(target, rhs)); + slots[last] = Some(Assignment::new(target, rhs)); } Ok(slots.into_iter().flatten().collect()) } +/// Substitute the value established by earlier writes to `name` 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], + name: &str, +) -> Result<(), AnalyzerError> { + let first = *indices.first().expect("group is non-empty"); + let last = *indices.last().expect("group is non-empty"); + let mut established: Option = None; + let mut write_index = 0; + + for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { + let is_target_write = indices.get(write_index) == Some(&index); + let mut env = HashMap::default(); + if let Some(value) = &established { + env.insert(name.to_string(), value.clone()); + guarded_assignment.condition = guarded_assignment + .condition + .take() + .map(|condition| substitute_expr_idents(condition, &env)); + let assignment = guarded_assignment.assignment.clone(); + guarded_assignment.assignment = Assignment::new( + assignment.lhs_value().clone(), + substitute_expr_idents(assignment.rhs, &env), + ); + } else if !is_target_write + && (guarded_assignment + .condition() + .is_some_and(|condition| expr_references_signal(condition, Some(name))) + || expr_references_signal(guarded_assignment.assignment().rhs(), Some(name))) + { + return Err(AnalyzerError::Unsupported( + "read-before-write dependency inside always_comb".to_string(), + )); + } + + if !is_target_write { + continue; + } + write_index += 1; + let write = &*guarded_assignment; + let value = write.assignment().rhs().clone(); + established = match (write.condition(), established) { + (None, _) => Some(value), + (Some(condition), Some(previous)) => Some(Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(previous), + }), + (Some(_), None) => None, + }; + } + Ok(()) +} + fn lvalue_self_reference(target: &LValue) -> Expr { match target { LValue::Ident(name) => Expr::Ident(name.clone()), @@ -7775,7 +7919,10 @@ fn conditional_assignments_from_conditional_statement( } if let Some((_, branch)) = &stmt.nodes.5 { - let condition = if exhaustive_fallback { + // Only a final else whose parent chain is already tautological is + // itself tautological; nested statements inside it inherit that. + let exhaustive = exhaustive_fallback && parent_condition.is_none(); + let condition = if exhaustive { None } else { combine_expr_condition_terms(parent_condition, prior_false) @@ -7783,7 +7930,7 @@ fn conditional_assignments_from_conditional_statement( conditional_assignments_from_statement_or_null( branch, condition, - false, + exhaustive, syntax_tree, const_env, packed_dimensions, @@ -7863,10 +8010,12 @@ fn conditional_assignments_from_case_statement( 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. conditional_assignments_from_statement_or_null( branch, condition, - exhaustive_fallback, + false, syntax_tree, const_env, packed_dimensions, @@ -7879,7 +8028,10 @@ fn conditional_assignments_from_case_statement( } if let Some(branch) = default_branch { - let condition = if exhaustive_fallback { + // Only a default whose parent chain is already tautological is + // itself tautological; nested statements inside it inherit that. + let exhaustive = exhaustive_fallback && parent_condition.is_none(); + let condition = if exhaustive { None } else { combine_expr_condition_terms(parent_condition, prior_false) @@ -7887,7 +8039,7 @@ fn conditional_assignments_from_case_statement( conditional_assignments_from_statement_or_null( branch, condition, - false, + exhaustive, syntax_tree, const_env, packed_dimensions, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 9bcadcd6e..619bc9d62 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -155,6 +155,215 @@ 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 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 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() + ); + } + + 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 analyzes_basic_sv_module_name() { let ir = analyze_source( 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()); + } +} From 12060c7a7bf55c1d1ee346ab500b30b98d190dcd Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 01:11:24 +0900 Subject: [PATCH 03/55] fix(sv-frontend): address follow-up enum review --- crates/celox-sv-analyzer/src/ast.rs | 607 +++++++++++++++++++--------- crates/celox-sv-analyzer/src/lib.rs | 156 +++++++ 2 files changed, 565 insertions(+), 198 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 7d2e4d36b..277b7d63b 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -149,7 +149,8 @@ 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 type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; + let mut parameters = parameters_from_module_node(node.clone(), syntax_tree, &type_aliases)?; let mut parameter_names = HashSet::default(); if let Some(parameter) = parameters .iter() @@ -164,12 +165,19 @@ impl Module { apply_parameter_overrides(&mut parameters, parameter_overrides)?; } let mut const_env = const_env_from_parameters(¶meters); - let enum_constants = - enum_member_constants_from_module_node(node.clone(), syntax_tree, &const_env)?; + let enum_constants = enum_member_constants_from_module_node( + node.clone(), + syntax_tree, + &const_env, + &type_aliases, + )?; 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); + } } - 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 port_names = HashSet::default(); @@ -615,7 +623,14 @@ 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) { @@ -623,7 +638,9 @@ fn cast_target_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 { @@ -742,9 +759,11 @@ fn constant_cast_const_expr( const_env: &HashMap, type_aliases: &HashMap, ) -> Option { - let ConstExpr::Literal(literal) = const_expr_from_ref_node( + let ConstExpr::Literal(literal) = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&cast.nodes.2.nodes.1), syntax_tree, + const_env, + type_aliases, )? else { return None; @@ -2207,6 +2226,7 @@ impl FfEvent { pub struct ConditionalAssignment { condition: Option, assignment: Assignment, + fills_exhaustive_fallback: bool, } impl ConditionalAssignment { @@ -2214,6 +2234,7 @@ impl ConditionalAssignment { Self { condition, assignment, + fills_exhaustive_fallback: false, } } @@ -2390,7 +2411,9 @@ fn ports_from_module_node( fn parameters_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, + type_aliases: &HashMap, ) -> Result, AnalyzerError> { + let base_const_env = HashMap::default(); let mut parameters = Vec::new(); if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { parameters_from_ref_node( @@ -2398,6 +2421,8 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, false, + &base_const_env, + type_aliases, )?; let mut local_parameters = Vec::new(); for child in parameter_port_list { @@ -2407,6 +2432,8 @@ fn parameters_from_module_node( syntax_tree, &mut local_parameters, true, + &base_const_env, + type_aliases, )?; } } @@ -2430,6 +2457,8 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, true, + &base_const_env, + type_aliases, )?, sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { parameters_from_ref_node( @@ -2437,6 +2466,8 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, false, + &base_const_env, + type_aliases, )? } _ => {} @@ -2973,6 +3004,8 @@ fn parameters_from_ref_node( syntax_tree: &SyntaxTree, parameters: &mut Vec, is_local: bool, + base_const_env: &HashMap, + type_aliases: &HashMap, ) -> Result<(), AnalyzerError> { if node.clone().into_iter().any(|child| { matches!( @@ -3008,11 +3041,11 @@ fn parameters_from_ref_node( 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 - .nodes - .2 - .as_ref() - .and_then(|(_, expr)| const_expr_from_constant_param(expr, 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); parameters.push(Parameter::new( @@ -3122,6 +3155,8 @@ struct EnumMemberConstants { 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` @@ -3132,6 +3167,7 @@ fn enum_member_constants_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, base_const_env: &HashMap, + type_aliases: &HashMap, ) -> Result { let mut constants = EnumMemberConstants::default(); let mut eval_env = base_const_env.clone(); @@ -3151,6 +3187,18 @@ fn enum_member_constants_from_module_node( 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(RefNode::EnumBaseType(base), syntax_tree) + .or_else(|| { + type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, 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(|| { @@ -3161,20 +3209,27 @@ fn enum_member_constants_from_module_node( "enum member `{name}` without an explicit value" ))); }; - let value = const_expr_from_ref_node(RefNode::ConstantExpression(value), syntax_tree) - .ok_or_else(|| { - AnalyzerError::Unsupported(format!("enum member `{name}` value")) + let value = const_expr_from_ref_node_with_env( + RefNode::ConstantExpression(value), + syntax_tree, + &eval_env, + type_aliases, + ) + .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` value")))?; + let number = eval_ast_const_expr(&value, &eval_env).ok_or_else(|| { + AnalyzerError::Unsupported(format!("unresolved enum member `{name}` value")) })?; - if let Some(number) = eval_ast_const_expr(&value, &eval_env) { - constants.numbers.insert(name.clone(), number); - eval_env.insert(name.clone(), number); - } - let expr = substitute_expr_constants_with_parameter_literals( - const_expr_to_expr(value), - base_const_env, - &constants.exprs, + constants.numbers.insert(name.clone(), number); + eval_env.insert(name.clone(), number); + constants.types.insert(name.clone(), member_type); + constants.exprs.insert( + name, + Expr::Literal(format_typed_parameter_literal( + number, + member_type.width, + member_type.signed, + )), ); - constants.exprs.insert(name, expr); } } Ok(constants) @@ -3758,6 +3813,10 @@ fn parameter_marker(name: &str) -> String { format!("__parameter::{name}") } +fn enum_marker(name: &str) -> String { + format!("__enum::{name}") +} + fn parameter_width_marker(name: &str) -> String { format!("__parameter::width::{name}") } @@ -6126,6 +6185,8 @@ fn generate_block_direct_local_parameter_names( syntax_tree, &mut parameters, true, + &HashMap::default(), + &HashMap::default(), ) .is_ok() { @@ -6320,6 +6381,8 @@ fn add_localparams_from_generate_item_with_literals( syntax_tree, &mut parameters, true, + const_env, + &HashMap::default(), ) .is_err() { @@ -6443,10 +6506,22 @@ fn substitute_expr_constants_with_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())) + 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), @@ -7138,54 +7213,30 @@ fn comb_assignments_from_guarded( continue; } let last = *indices.last().expect("group is non-empty"); - let name = write_target_base_name(&target); - if let LValue::Ident(name) = &target { - substitute_intermediate_comb_value_reads(&mut guarded, &indices, name)?; - } - let mut current: Option = None; - if guarded[indices[0]].condition().is_none() { - // A leading unconditional write establishes the procedural - // default. Later guarded writes override it, just as blocking - // assignments do in source order. - for index in &indices { - let write = &guarded[*index]; - let value = write.assignment().rhs().clone(); - current = Some(match write.condition() { + substitute_intermediate_comb_value_reads(&mut guarded, &indices, &target)?; + // 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 mut current = lvalue_self_reference(&target); + for index in &indices { + let write = &guarded[*index]; + let value = write.assignment().rhs().clone(); + current = if write.fills_exhaustive_fallback { + substitute_expr_lvalue(current, &target, &value) + } else { + match write.condition() { None => value, Some(condition) => Expr::Mux { condition: Box::new(condition.clone()), then_expr: Box::new(value), - else_expr: Box::new(current.expect("leading default establishes a value")), + else_expr: Box::new(current), }, - }); - } - } else { - // Branches are emitted in priority order, with an exhaustive - // else/default represented as the final unconditional fallback. - for index in indices.iter().rev() { - let write = &guarded[*index]; - let value = write.assignment().rhs().clone(); - current = Some(match write.condition() { - None => value, - Some(condition) => { - // A conditional write with no later unconditional - // fallback would keep the previous value, which - // infers a latch; represent that tentatively with a - // self-reference and reject it below. - let otherwise = current.unwrap_or_else(|| { - lvalue_self_reference(write.assignment().lhs_value()) - }); - Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(value), - else_expr: Box::new(otherwise), - } - } - }); - } + } + }; } - let rhs = current.expect("guarded writes exist"); - if expr_references_signal(&rhs, name) { + let rhs = current; + if expr_references_lvalue(&rhs, &target) { return Err(AnalyzerError::Unsupported( "latch inference inside always_comb".to_string(), )); @@ -7195,39 +7246,38 @@ fn comb_assignments_from_guarded( Ok(slots.into_iter().flatten().collect()) } -/// Substitute the value established by earlier writes to `name` into reads +/// 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], - name: &str, + target: &LValue, ) -> Result<(), AnalyzerError> { let first = *indices.first().expect("group is non-empty"); let last = *indices.last().expect("group is non-empty"); - let mut established: Option = None; + let mut established = lvalue_self_reference(target); + let mut initialized = false; let mut write_index = 0; for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { let is_target_write = indices.get(write_index) == Some(&index); - let mut env = HashMap::default(); - if let Some(value) = &established { - env.insert(name.to_string(), value.clone()); + if initialized { guarded_assignment.condition = guarded_assignment .condition .take() - .map(|condition| substitute_expr_idents(condition, &env)); + .map(|condition| substitute_expr_lvalue(condition, target, &established)); let assignment = guarded_assignment.assignment.clone(); guarded_assignment.assignment = Assignment::new( assignment.lhs_value().clone(), - substitute_expr_idents(assignment.rhs, &env), + substitute_expr_lvalue(assignment.rhs, target, &established), ); } else if !is_target_write && (guarded_assignment .condition() - .is_some_and(|condition| expr_references_signal(condition, Some(name))) - || expr_references_signal(guarded_assignment.assignment().rhs(), Some(name))) + .is_some_and(|condition| expr_references_lvalue(condition, target)) + || expr_references_lvalue(guarded_assignment.assignment().rhs(), target)) { return Err(AnalyzerError::Unsupported( "read-before-write dependency inside always_comb".to_string(), @@ -7240,127 +7290,194 @@ fn substitute_intermediate_comb_value_reads( write_index += 1; let write = &*guarded_assignment; let value = write.assignment().rhs().clone(); - established = match (write.condition(), established) { - (None, _) => Some(value), - (Some(condition), Some(previous)) => Some(Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(value), - else_expr: Box::new(previous), - }), - (Some(_), None) => None, - }; + if write.fills_exhaustive_fallback { + established = substitute_expr_lvalue(established, target, &value); + initialized = true; + } else { + established = match write.condition() { + None => { + initialized = true; + value + } + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(established), + }, + }; + } } Ok(()) } -fn lvalue_self_reference(target: &LValue) -> Expr { - match target { - LValue::Ident(name) => Expr::Ident(name.clone()), - LValue::Select { - name, +fn substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { + if expr_matches_lvalue(&expr, target) { + return value.clone(); + } + match expr { + Expr::Ident(_) | Expr::Literal(_) => expr, + Expr::Select { + expr, msb, lsb, signed, - .. } => Expr::Select { - expr: Box::new(Expr::Ident(name.clone())), - msb: msb.clone(), - lsb: lsb.clone(), - signed: *signed, + expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + msb, + lsb, + signed, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .into_iter() + .map(|part| substitute_expr_lvalue(part, target, value)) + .collect(), + ), + Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { + count, + parts: parts + .into_iter() + .map(|part| substitute_expr_lvalue(part, target, value)) + .collect(), + }, + Expr::Resize { + expr, + width, + signed, + } => Expr::Resize { + expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + width, + signed, + }, + Expr::Unary { op, expr } => Expr::Unary { + op, + expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new(substitute_expr_lvalue(*left, target, value)), + op, + right: Box::new(substitute_expr_lvalue(*right, target, value)), + }, + Expr::Mux { + condition, + then_expr, + else_expr, + } => Expr::Mux { + condition: Box::new(substitute_expr_lvalue(*condition, target, value)), + then_expr: Box::new(substitute_expr_lvalue(*then_expr, target, value)), + else_expr: Box::new(substitute_expr_lvalue(*else_expr, target, value)), + }, + Expr::Call { name, args } => Expr::Call { + name, + args: args + .into_iter() + .map(|arg| substitute_expr_lvalue(arg, target, value)) + .collect(), }, } } -fn write_target_base_name(target: &LValue) -> Option<&str> { - match target { - LValue::Ident(name) => Some(name), - LValue::Select { name, .. } => Some(name), +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_signal(expr: &Expr, signal: Option<&str>) -> bool { - let Some(signal) = signal else { - return false; - }; +fn expr_references_lvalue(expr: &Expr, target: &LValue) -> bool { + if expr_matches_lvalue(expr, target) { + return true; + } match expr { - Expr::Ident(name) => name == signal, - Expr::Literal(_) => false, - Expr::Select { expr, msb, lsb, .. } => { - expr_references_signal(expr, Some(signal)) - || const_expr_references_signal(msb, signal) - || const_expr_references_signal(lsb, signal) - } - Expr::Concat(parts) => parts - .iter() - .any(|part| expr_references_signal(part, Some(signal))), - Expr::RepeatConcat { parts, .. } => parts - .iter() - .any(|part| expr_references_signal(part, Some(signal))), - Expr::Resize { expr, .. } => expr_references_signal(expr, Some(signal)), - Expr::Call { args, .. } => args + Expr::Ident(_) | Expr::Literal(_) => false, + Expr::Select { expr, .. } | Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { + expr_references_lvalue(expr, target) + } + Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts .iter() - .any(|arg| expr_references_signal(arg, Some(signal))), - Expr::Unary { expr, .. } => expr_references_signal(expr, Some(signal)), + .any(|part| expr_references_lvalue(part, target)), Expr::Binary { left, right, .. } => { - expr_references_signal(left, Some(signal)) - || expr_references_signal(right, Some(signal)) + expr_references_lvalue(left, target) || expr_references_lvalue(right, target) } Expr::Mux { condition, then_expr, else_expr, } => { - expr_references_signal(condition, Some(signal)) - || expr_references_signal(then_expr, Some(signal)) - || expr_references_signal(else_expr, Some(signal)) + expr_references_lvalue(condition, target) + || expr_references_lvalue(then_expr, target) + || expr_references_lvalue(else_expr, target) } + Expr::Call { args, .. } => args.iter().any(|arg| expr_references_lvalue(arg, target)), } } -fn const_expr_references_signal(expr: &ConstExpr, signal: &str) -> bool { - match expr { - ConstExpr::Ident(name) => name == signal, - ConstExpr::Literal(_) => false, - ConstExpr::Select { expr, bit } => { - const_expr_references_signal(expr, signal) || const_expr_references_signal(bit, signal) - } - ConstExpr::Function { args, .. } => args - .iter() - .any(|arg| const_expr_references_signal(arg, signal)), - ConstExpr::Unary { expr, .. } => const_expr_references_signal(expr, signal), - ConstExpr::Binary { left, right, .. } => { - const_expr_references_signal(left, signal) - || const_expr_references_signal(right, signal) - } - ConstExpr::Mux { - condition, - then_expr, - else_expr, - } => { - const_expr_references_signal(condition, signal) - || const_expr_references_signal(then_expr, signal) - || const_expr_references_signal(else_expr, signal) - } +fn lvalue_self_reference(target: &LValue) -> Expr { + match target { + LValue::Ident(name) => Expr::Ident(name.clone()), + LValue::Select { + name, + msb, + lsb, + signed, + .. + } => Expr::Select { + expr: Box::new(Expr::Ident(name.clone())), + msb: msb.clone(), + lsb: lsb.clone(), + signed: *signed, + }, } } fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), AnalyzerError> { match &stmt.nodes.2 { - sv_parser::StatementItem::BlockingAssignment(_) - | sv_parser::StatementItem::ConditionalStatement(_) => Ok(()), + 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(_)) { - Ok(()) - } else { - Err(AnalyzerError::Unsupported( + if !matches!(case.nodes.1, sv_parser::CaseKeyword::Case(_)) { + return Err(AnalyzerError::Unsupported( "casez or casex inside always_comb".to_string(), - )) + )); } + 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::StatementItem::SeqBlock(block) => { for stmt in &block.nodes.3 { @@ -7376,6 +7493,15 @@ fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), Ana } } +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, @@ -7885,7 +8011,9 @@ fn conditional_assignments_from_conditional_statement( AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) })?; let mut prior_false = Vec::new(); + let mut branch_targets = Vec::new(); let then_condition = combine_expr_conditions(parent_condition.clone(), if_condition.clone()); + let branch_start = assignments.len(); conditional_assignments_from_statement_or_null( &stmt.nodes.3, then_condition, @@ -7895,6 +8023,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; + branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); prior_false.push(procedural_false_condition(if_condition)); for (_, _, predicate, branch) in &stmt.nodes.4 { @@ -7906,6 +8035,7 @@ fn conditional_assignments_from_conditional_statement( let mut terms = prior_false.clone(); terms.push(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, @@ -7915,27 +8045,29 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; + branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); prior_false.push(procedural_false_condition(branch_condition)); } if let Some((_, branch)) = &stmt.nodes.5 { - // Only a final else whose parent chain is already tautological is - // itself tautological; nested statements inside it inherit that. + // 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 = if exhaustive { - None - } else { - combine_expr_condition_terms(parent_condition, prior_false) - }; + let condition = combine_expr_condition_terms(parent_condition, prior_false); + let branch_start = assignments.len(); conditional_assignments_from_statement_or_null( branch, - condition, - exhaustive, + condition.clone(), + false, syntax_tree, const_env, packed_dimensions, assignments, )?; + branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); + if exhaustive { + mark_exhaustive_fallback(&mut assignments[branch_start..], &branch_targets); + } } Ok(()) } @@ -7960,10 +8092,14 @@ fn conditional_assignments_from_case_statement( assignments: &mut Vec, ) -> Result<(), AnalyzerError> { let sv_parser::CaseStatement::Normal(stmt) = stmt else { - return Ok(()); + return Err(AnalyzerError::Unsupported( + "casez, casex, or pattern case inside always_comb".to_string(), + )); }; if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { - return Ok(()); + 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, @@ -8006,12 +8142,14 @@ fn conditional_assignments_from_case_statement( } let mut prior_false = Vec::new(); + let mut branch_targets = 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); // 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, @@ -8021,6 +8159,7 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; + branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); prior_false.push(Expr::Unary { op: UnaryOp::LogicNot, expr: Box::new(branch_condition), @@ -8028,27 +8167,57 @@ fn conditional_assignments_from_case_statement( } if let Some(branch) = default_branch { - // Only a default whose parent chain is already tautological is - // itself tautological; nested statements inside it inherit that. + // 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 = if exhaustive { - None - } else { - combine_expr_condition_terms(parent_condition, prior_false) - }; + let condition = combine_expr_condition_terms(parent_condition, prior_false); + let branch_start = assignments.len(); conditional_assignments_from_statement_or_null( branch, - condition, - exhaustive, + condition.clone(), + false, syntax_tree, const_env, packed_dimensions, assignments, )?; + branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); + if exhaustive { + mark_exhaustive_fallback(&mut assignments[branch_start..], &branch_targets); + } } Ok(()) } +fn conditional_assignment_targets(assignments: &[ConditionalAssignment]) -> Vec { + let mut targets = Vec::new(); + for assignment in assignments { + let target = assignment.assignment().lhs_value(); + if !targets.contains(target) { + targets.push(target.clone()); + } + } + targets +} + +fn mark_exhaustive_fallback( + fallback_assignments: &mut [ConditionalAssignment], + branch_targets: &[Vec], +) { + let mut marked_targets = Vec::new(); + for assignment in fallback_assignments { + let target = assignment.assignment().lhs_value(); + if branch_targets + .iter() + .all(|targets| targets.contains(target)) + && !marked_targets.contains(target) + { + marked_targets.push(target.clone()); + assignment.fills_exhaustive_fallback = true; + } + } +} + fn expr_from_cond_predicate( predicate: &sv_parser::CondPredicate, syntax_tree: &SyntaxTree, @@ -9656,14 +9825,21 @@ fn expr_to_const(expr: Expr) -> Option { } } -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, }, @@ -9973,16 +10149,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, @@ -9991,14 +10183,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), @@ -10012,7 +10208,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, }, @@ -10049,15 +10250,17 @@ fn const_expr_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Opti .map(ConstExpr::Ident) }) } - sv_parser::ConstantPrimary::ConstantCast(cast) => constant_cast_const_expr( - cast, - syntax_tree, - &HashMap::default(), - &HashMap::default(), - ), + sv_parser::ConstantPrimary::ConstantCast(cast) => { + 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, }, @@ -10089,22 +10292,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, )?), }) } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 619bc9d62..8f7c0cf9c 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -218,6 +218,87 @@ mod tests { assert!(!expr_references_ident_name(y.rhs(), "x")); } + #[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 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 sign_extends_negative_literals_in_widening_constant_casts() { let ir = analyze_source( @@ -250,6 +331,39 @@ mod tests { assert_eq!(ir.modules()[0].parameters()[1].resolved_value(), Some(7)); } + #[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 resolves_enum_members_referencing_earlier_members() { let ir = analyze_source( @@ -279,6 +393,48 @@ mod tests { ); } + #[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, From b75c43322ca596146636bdfb537cca02e73fe228 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 02:19:56 +0900 Subject: [PATCH 04/55] fix(sv-frontend): address additional review feedback --- crates/celox-sv-analyzer/src/ast.rs | 526 +++++++++++++++++++++++----- crates/celox-sv-analyzer/src/lib.rs | 142 ++++++++ 2 files changed, 590 insertions(+), 78 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 277b7d63b..bf144f5a5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -178,6 +178,7 @@ impl Module { insert_parameter_type_markers(&mut const_env, name, *r#type); } } + extend_const_env_with_parameters(&mut const_env, ¶meters); reject_silently_ignored_constructs(node.clone(), syntax_tree, &const_env, &type_aliases)?; let ports = ports_from_module_node(node.clone(), syntax_tree)?; let mut port_names = HashSet::default(); @@ -759,19 +760,17 @@ fn constant_cast_const_expr( const_env: &HashMap, type_aliases: &HashMap, ) -> Option { - let ConstExpr::Literal(literal) = const_expr_from_ref_node_with_env( + let operand = const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&cast.nodes.2.nodes.1), syntax_tree, const_env, type_aliases, - )? - else { - return None; - }; - let literal = typecheck::parse_integral_literal(&literal)?; - if literal.mask != 0u8.into() { - return None; - } + )?; + let operand_type = infer_const_expr_type(&operand, ¶meter_types_from_const_env(const_env))?; + 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); + let literal = typecheck::parse_integral_literal(&operand_literal)?; let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; // A size cast keeps the source expression's signedness when the target // is described by a constant primary; a type cast takes the target's. @@ -2657,16 +2656,16 @@ fn signals_from_generate_block( 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); + 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_const_expr_constants(range.left.clone(), const_env); - range.right = substitute_const_expr_constants(range.right.clone(), const_env); + 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_const_expr_constants(size, const_env)); + .map(|size| substitute_dimension_constants(size, const_env)); } } } @@ -3117,19 +3116,26 @@ fn normalize_unbased_unsized_parameter_value( fn const_env_from_parameters(parameters: &[Parameter]) -> HashMap { let mut env = HashMap::default(); - let mut parameter_types = HashMap::default(); + extend_const_env_with_parameters(&mut env, parameters); + env +} + +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 { + 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); + 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); + } } - env } fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 { @@ -3813,6 +3819,10 @@ fn parameter_marker(name: &str) -> String { format!("__parameter::{name}") } +fn local_parameter_marker(name: &str) -> String { + format!("__parameter::local::{name}") +} + fn enum_marker(name: &str) -> String { format!("__enum::{name}") } @@ -7052,42 +7062,92 @@ fn substitute_expr_idents(expr: Expr, env: &HashMap) -> Expr { fn substitute_const_expr_constants( expr: ConstExpr, const_env: &HashMap, +) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, false) +} + +fn substitute_dimension_constants(expr: ConstExpr, const_env: &HashMap) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, true) +} + +fn substitute_const_expr_constants_impl( + expr: ConstExpr, + const_env: &HashMap, + include_local_parameters: bool, ) -> ConstExpr { match expr { ConstExpr::Ident(name) => const_env .get(&name) - .filter(|_| !const_env.contains_key(¶meter_marker(&name))) + .filter(|_| { + !const_env.contains_key(¶meter_marker(&name)) + || include_local_parameters + && const_env.contains_key(&local_parameter_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)), + expr: Box::new(substitute_const_expr_constants_impl( + *expr, + const_env, + include_local_parameters, + )), + bit: Box::new(substitute_const_expr_constants_impl( + *bit, + const_env, + include_local_parameters, + )), }, ConstExpr::Function { name, args } => ConstExpr::Function { name, args: args .into_iter() - .map(|arg| substitute_const_expr_constants(arg, const_env)) + .map(|arg| { + substitute_const_expr_constants_impl(arg, const_env, include_local_parameters) + }) .collect(), }, ConstExpr::Unary { op, expr } => ConstExpr::Unary { op, - expr: Box::new(substitute_const_expr_constants(*expr, const_env)), + expr: Box::new(substitute_const_expr_constants_impl( + *expr, + const_env, + include_local_parameters, + )), }, ConstExpr::Binary { left, op, right } => ConstExpr::Binary { - left: Box::new(substitute_const_expr_constants(*left, const_env)), + left: Box::new(substitute_const_expr_constants_impl( + *left, + const_env, + include_local_parameters, + )), op, - right: Box::new(substitute_const_expr_constants(*right, const_env)), + right: Box::new(substitute_const_expr_constants_impl( + *right, + const_env, + include_local_parameters, + )), }, ConstExpr::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)), + condition: Box::new(substitute_const_expr_constants_impl( + *condition, + const_env, + include_local_parameters, + )), + then_expr: Box::new(substitute_const_expr_constants_impl( + *then_expr, + const_env, + include_local_parameters, + )), + else_expr: Box::new(substitute_const_expr_constants_impl( + *else_expr, + const_env, + include_local_parameters, + )), }, } } @@ -7188,7 +7248,22 @@ fn comb_assignments_from_guarded( let mut groups: Vec> = Vec::new(); for (index, conditional) in guarded.iter().enumerate() { let target = conditional.assignment().lhs_value(); - match targets.iter().position(|existing| existing == target) { + 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) + && 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()); @@ -7196,10 +7271,23 @@ fn comb_assignments_from_guarded( } } } + // 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. + for (target, indices) in targets.iter().zip(&groups) { + if indices + .iter() + .all(|index| guarded[*index].condition().is_none()) + { + continue; + } + let initial = overlapping_whole_value_before(&guarded, indices[0], target); + substitute_intermediate_comb_value_reads(&mut guarded, indices, target, initial)?; + } + // Merged groups land on the slot of their last write so relative - // statement ordering across different targets is preserved. Substitute - // values established by earlier writes into intervening reads before the - // group is moved, preserving blocking-assignment statement semantics. + // 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) { @@ -7213,17 +7301,17 @@ fn comb_assignments_from_guarded( continue; } let last = *indices.last().expect("group is non-empty"); - substitute_intermediate_comb_value_reads(&mut guarded, &indices, &target)?; // 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 mut current = lvalue_self_reference(&target); + let mut current = overlapping_whole_value_before(&guarded, indices[0], &target) + .unwrap_or_else(comb_previous_value_placeholder); for index in &indices { let write = &guarded[*index]; let value = write.assignment().rhs().clone(); current = if write.fills_exhaustive_fallback { - substitute_expr_lvalue(current, &target, &value) + substitute_comb_previous_value(current, &value) } else { match write.condition() { None => value, @@ -7236,7 +7324,9 @@ fn comb_assignments_from_guarded( }; } let rhs = current; - if expr_references_lvalue(&rhs, &target) { + if expr_contains_comb_previous_value(&rhs) + || expr_references_overlapping_lvalue(&rhs, &target) + { return Err(AnalyzerError::Unsupported( "latch inference inside always_comb".to_string(), )); @@ -7254,11 +7344,12 @@ fn substitute_intermediate_comb_value_reads( guarded: &mut [ConditionalAssignment], indices: &[usize], target: &LValue, + initial: Option, ) -> Result<(), AnalyzerError> { let first = *indices.first().expect("group is non-empty"); let last = *indices.last().expect("group is non-empty"); - let mut established = lvalue_self_reference(target); - let mut initialized = false; + let mut initialized = initial.is_some(); + let mut established = initial.unwrap_or_else(comb_previous_value_placeholder); let mut write_index = 0; for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { @@ -7291,7 +7382,7 @@ fn substitute_intermediate_comb_value_reads( let write = &*guarded_assignment; let value = write.assignment().rhs().clone(); if write.fills_exhaustive_fallback { - established = substitute_expr_lvalue(established, target, &value); + established = substitute_comb_previous_value(established, &value); initialized = true; } else { established = match write.condition() { @@ -7310,6 +7401,113 @@ fn substitute_intermediate_comb_value_reads( Ok(()) } +fn lvalues_overlap(left: &LValue, right: &LValue) -> 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, + .. + }, + LValue::Select { + name: right_name, + msb: right_msb, + lsb: right_lsb, + .. + }, + ) => left_name == right_name && left_msb == right_msb && left_lsb == right_lsb, + } +} + +fn overlapping_whole_value_before( + guarded: &[ConditionalAssignment], + before: usize, + target: &LValue, +) -> Option { + let LValue::Select { + name, + msb, + lsb, + signed, + .. + } = target + else { + return None; + }; + let mut current = comb_previous_value_placeholder(); + let mut initialized = false; + for write in &guarded[..before] { + if !matches!(write.assignment().lhs_value(), LValue::Ident(whole) if whole == name) { + continue; + } + let value = write.assignment().rhs().clone(); + if write.fills_exhaustive_fallback { + current = substitute_comb_previous_value(current, &value); + initialized = true; + } else { + current = match write.condition() { + None => { + initialized = true; + value + } + Some(condition) => Expr::Mux { + condition: Box::new(condition.clone()), + then_expr: Box::new(value), + else_expr: Box::new(current), + }, + }; + } + } + initialized.then_some(Expr::Select { + expr: Box::new(current), + msb: msb.clone(), + lsb: lsb.clone(), + signed: *signed, + }) +} + +const COMB_PREVIOUS_VALUE: &str = "\0celox_comb_previous_value"; + +fn comb_previous_value_placeholder() -> Expr { + Expr::Ident(COMB_PREVIOUS_VALUE.to_string()) +} + +fn substitute_comb_previous_value(expr: Expr, value: &Expr) -> Expr { + let mut env = HashMap::default(); + env.insert(COMB_PREVIOUS_VALUE.to_string(), value.clone()); + substitute_expr_idents(expr, &env) +} + +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 substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { if expr_matches_lvalue(&expr, target) { return value.clone(); @@ -7425,21 +7623,47 @@ fn expr_references_lvalue(expr: &Expr, target: &LValue) -> bool { } } -fn lvalue_self_reference(target: &LValue) -> Expr { - match target { - LValue::Ident(name) => Expr::Ident(name.clone()), - LValue::Select { - name, - msb, - lsb, - signed, - .. - } => Expr::Select { - expr: Box::new(Expr::Ident(name.clone())), - msb: msb.clone(), - lsb: lsb.clone(), - signed: *signed, +fn expr_references_overlapping_lvalue(expr: &Expr, target: &LValue) -> 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, .. } => { + if matches!((&**expr, target), (Expr::Ident(_), LValue::Select { .. })) { + false + } else { + expr_references_overlapping_lvalue(expr, target) + } + } + Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { + expr_references_overlapping_lvalue(expr, target) + } + Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts + .iter() + .any(|part| expr_references_overlapping_lvalue(part, target)), + Expr::Binary { left, right, .. } => { + expr_references_overlapping_lvalue(left, target) + || expr_references_overlapping_lvalue(right, target) + } + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_references_overlapping_lvalue(condition, target) + || expr_references_overlapping_lvalue(then_expr, target) + || expr_references_overlapping_lvalue(else_expr, target) + } + Expr::Call { args, .. } => args + .iter() + .any(|arg| expr_references_overlapping_lvalue(arg, target)), } } @@ -8011,9 +8235,8 @@ fn conditional_assignments_from_conditional_statement( AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) })?; let mut prior_false = Vec::new(); - let mut branch_targets = Vec::new(); + let mut definitely_assigned_branches = Vec::new(); let then_condition = combine_expr_conditions(parent_condition.clone(), if_condition.clone()); - let branch_start = assignments.len(); conditional_assignments_from_statement_or_null( &stmt.nodes.3, then_condition, @@ -8023,7 +8246,11 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - branch_targets.push(conditional_assignment_targets(&assignments[branch_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 { @@ -8035,7 +8262,6 @@ fn conditional_assignments_from_conditional_statement( let mut terms = prior_false.clone(); terms.push(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, @@ -8045,7 +8271,11 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - branch_targets.push(conditional_assignment_targets(&assignments[branch_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)); } @@ -8064,9 +8294,16 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - branch_targets.push(conditional_assignment_targets(&assignments[branch_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..], &branch_targets); + mark_exhaustive_fallback( + &mut assignments[branch_start..], + &definitely_assigned_branches, + ); } } Ok(()) @@ -8142,14 +8379,13 @@ fn conditional_assignments_from_case_statement( } let mut prior_false = Vec::new(); - let mut branch_targets = Vec::new(); + let mut definitely_assigned_branches = 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); // 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, @@ -8159,7 +8395,11 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; - branch_targets.push(conditional_assignment_targets(&assignments[branch_start..])); + definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + )); prior_false.push(Expr::Unary { op: UnaryOp::LogicNot, expr: Box::new(branch_condition), @@ -8181,25 +8421,21 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; - branch_targets.push(conditional_assignment_targets(&assignments[branch_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..], &branch_targets); + mark_exhaustive_fallback( + &mut assignments[branch_start..], + &definitely_assigned_branches, + ); } } Ok(()) } -fn conditional_assignment_targets(assignments: &[ConditionalAssignment]) -> Vec { - let mut targets = Vec::new(); - for assignment in assignments { - let target = assignment.assignment().lhs_value(); - if !targets.contains(target) { - targets.push(target.clone()); - } - } - targets -} - fn mark_exhaustive_fallback( fallback_assignments: &mut [ConditionalAssignment], branch_targets: &[Vec], @@ -8218,18 +8454,152 @@ fn mark_exhaustive_fallback( } } +fn definitely_assigned_comb_targets_statement_or_null( + stmt: &sv_parser::StatementOrNull, + syntax_tree: &SyntaxTree, + 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 definitely_assigned_comb_targets( + stmt: &sv_parser::Statement, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, +) -> Vec { + 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, + }; + target.into_iter().collect() + } + sv_parser::StatementItem::SeqBlock(block) => { + let mut targets = Vec::new(); + for stmt in &block.nodes.3 { + for target in definitely_assigned_comb_targets_statement_or_null( + stmt, + syntax_tree, + packed_dimensions, + ) { + if !targets.contains(&target) { + targets.push(target); + } + } + } + targets + } + sv_parser::StatementItem::ConditionalStatement(conditional) => { + let Some((_, else_branch)) = &conditional.nodes.5 else { + return Vec::new(); + }; + let mut branches = vec![definitely_assigned_comb_targets_statement_or_null( + &conditional.nodes.3, + syntax_tree, + packed_dimensions, + )]; + branches.extend(conditional.nodes.4.iter().map(|(_, _, _, branch)| { + definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + ) + })); + branches.push(definitely_assigned_comb_targets_statement_or_null( + else_branch, + syntax_tree, + packed_dimensions, + )); + intersect_lvalue_sets(branches) + } + sv_parser::StatementItem::CaseStatement(case) => { + let sv_parser::CaseStatement::Normal(case) = &**case else { + return Vec::new(); + }; + let mut has_default = false; + let branches = std::iter::once(&case.nodes.3) + .chain(case.nodes.4.iter()) + .map(|item| { + let branch = match item { + sv_parser::CaseItem::NonDefault(item) => &item.nodes.2, + sv_parser::CaseItem::Default(item) => { + has_default = true; + &item.nodes.2 + } + }; + definitely_assigned_comb_targets_statement_or_null( + branch, + syntax_tree, + packed_dimensions, + ) + }) + .collect::>(); + if has_default { + intersect_lvalue_sets(branches) + } else { + Vec::new() + } + } + _ => Vec::new(), + } +} + +fn intersect_lvalue_sets(mut sets: Vec>) -> Vec { + let Some(mut intersection) = sets.pop() else { + return Vec::new(); + }; + intersection.retain(|target| sets.iter().all(|set| set.contains(target))); + intersection +} + fn expr_from_cond_predicate( predicate: &sv_parser::CondPredicate, 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]) } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 8f7c0cf9c..cb344e244 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -299,6 +299,88 @@ mod tests { ); } + #[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 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 sign_extends_negative_literals_in_widening_constant_casts() { let ir = analyze_source( @@ -364,6 +446,24 @@ mod tests { ); } + #[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( @@ -520,6 +620,48 @@ mod tests { 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 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( From 7c7f4e304649e36371554ea73305b4592617fc88 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 02:52:21 +0900 Subject: [PATCH 05/55] fix(sv-frontend): address latest review feedback --- crates/celox-sv-analyzer/src/ast.rs | 163 +++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 111 ++++++++++++ .../systemverilog/review_regressions.rs | 27 +++ 3 files changed, 280 insertions(+), 21 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index bf144f5a5..fef24c14b 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -7228,7 +7228,8 @@ fn comb_process_from_always_construct( packed_dimensions, &mut guarded_assignments, )?; - let assignments = comb_assignments_from_guarded(guarded_assignments)?; + let assignments = + comb_assignments_from_guarded(guarded_assignments, &packed_dimensions.const_env)?; Ok((!assignments.is_empty()) .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) } @@ -7243,6 +7244,7 @@ fn comb_process_from_always_construct( /// previous value, which infers a latch, and is rejected. fn comb_assignments_from_guarded( mut guarded: Vec, + const_env: &HashMap, ) -> Result, AnalyzerError> { let mut targets: Vec = Vec::new(); let mut groups: Vec> = Vec::new(); @@ -7282,7 +7284,13 @@ fn comb_assignments_from_guarded( continue; } let initial = overlapping_whole_value_before(&guarded, indices[0], target); - substitute_intermediate_comb_value_reads(&mut guarded, indices, target, initial)?; + substitute_intermediate_comb_value_reads( + &mut guarded, + indices, + target, + initial, + const_env, + )?; } // Merged groups land on the slot of their last write so relative @@ -7345,6 +7353,7 @@ fn substitute_intermediate_comb_value_reads( indices: &[usize], target: &LValue, initial: Option, + const_env: &HashMap, ) -> Result<(), AnalyzerError> { let first = *indices.first().expect("group is non-empty"); let last = *indices.last().expect("group is non-empty"); @@ -7355,14 +7364,13 @@ fn substitute_intermediate_comb_value_reads( for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { let is_target_write = indices.get(write_index) == Some(&index); if initialized { - guarded_assignment.condition = guarded_assignment - .condition - .take() - .map(|condition| substitute_expr_lvalue(condition, target, &established)); + guarded_assignment.condition = guarded_assignment.condition.take().map(|condition| { + substitute_expr_lvalue(condition, target, &established, const_env) + }); let assignment = guarded_assignment.assignment.clone(); guarded_assignment.assignment = Assignment::new( assignment.lhs_value().clone(), - substitute_expr_lvalue(assignment.rhs, target, &established), + substitute_expr_lvalue(assignment.rhs, target, &established, const_env), ); } else if !is_target_write && (guarded_assignment @@ -7508,10 +7516,19 @@ fn expr_contains_comb_previous_value(expr: &Expr) -> bool { } } -fn substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { +fn substitute_expr_lvalue( + expr: Expr, + target: &LValue, + value: &Expr, + const_env: &HashMap, +) -> Expr { if expr_matches_lvalue(&expr, target) { return value.clone(); } + if let Some(replacement) = substitute_overlapping_selected_read(&expr, target, value, const_env) + { + return replacement; + } match expr { Expr::Ident(_) | Expr::Literal(_) => expr, Expr::Select { @@ -7520,7 +7537,7 @@ fn substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { lsb, signed, } => Expr::Select { - expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + expr: Box::new(substitute_expr_lvalue(*expr, target, value, const_env)), msb, lsb, signed, @@ -7528,14 +7545,14 @@ fn substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { Expr::Concat(parts) => Expr::Concat( parts .into_iter() - .map(|part| substitute_expr_lvalue(part, target, value)) + .map(|part| substitute_expr_lvalue(part, target, value, const_env)) .collect(), ), Expr::RepeatConcat { count, parts } => Expr::RepeatConcat { count, parts: parts .into_iter() - .map(|part| substitute_expr_lvalue(part, target, value)) + .map(|part| substitute_expr_lvalue(part, target, value, const_env)) .collect(), }, Expr::Resize { @@ -7543,38 +7560,139 @@ fn substitute_expr_lvalue(expr: Expr, target: &LValue, value: &Expr) -> Expr { width, signed, } => Expr::Resize { - expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + expr: Box::new(substitute_expr_lvalue(*expr, target, value, const_env)), width, signed, }, Expr::Unary { op, expr } => Expr::Unary { op, - expr: Box::new(substitute_expr_lvalue(*expr, target, value)), + expr: Box::new(substitute_expr_lvalue(*expr, target, value, const_env)), }, Expr::Binary { left, op, right } => Expr::Binary { - left: Box::new(substitute_expr_lvalue(*left, target, value)), + left: Box::new(substitute_expr_lvalue(*left, target, value, const_env)), op, - right: Box::new(substitute_expr_lvalue(*right, target, value)), + right: Box::new(substitute_expr_lvalue(*right, target, value, const_env)), }, Expr::Mux { condition, then_expr, else_expr, } => Expr::Mux { - condition: Box::new(substitute_expr_lvalue(*condition, target, value)), - then_expr: Box::new(substitute_expr_lvalue(*then_expr, target, value)), - else_expr: Box::new(substitute_expr_lvalue(*else_expr, target, value)), + condition: Box::new(substitute_expr_lvalue(*condition, target, value, const_env)), + then_expr: Box::new(substitute_expr_lvalue(*then_expr, target, value, const_env)), + else_expr: Box::new(substitute_expr_lvalue(*else_expr, target, value, const_env)), }, Expr::Call { name, args } => Expr::Call { name, args: args .into_iter() - .map(|arg| substitute_expr_lvalue(arg, target, value)) + .map(|arg| substitute_expr_lvalue(arg, target, value, const_env)) .collect(), }, } } +fn substitute_overlapping_selected_read( + expr: &Expr, + target: &LValue, + value: &Expr, + const_env: &HashMap, +) -> Option { + let Expr::Select { + expr: read_base, + msb: read_msb, + lsb: read_lsb, + .. + } = expr + else { + return None; + }; + let Expr::Ident(read_name) = &**read_base else { + return None; + }; + let LValue::Select { + name: target_name, + msb: target_msb, + lsb: target_lsb, + .. + } = target + else { + return None; + }; + if read_name != target_name { + return None; + } + + 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 overlap_last = if read_step < 0 { + overlap_low + } else { + overlap_high + }; + 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 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, @@ -8236,7 +8354,10 @@ fn conditional_assignments_from_conditional_statement( })?; let mut prior_false = Vec::new(); let mut definitely_assigned_branches = Vec::new(); - let then_condition = combine_expr_conditions(parent_condition.clone(), if_condition.clone()); + let then_condition = combine_expr_conditions( + parent_condition.clone(), + procedural_truth_condition(if_condition.clone()), + ); conditional_assignments_from_statement_or_null( &stmt.nodes.3, then_condition, @@ -8260,7 +8381,7 @@ fn conditional_assignments_from_conditional_statement( AnalyzerError::Unsupported("always_ff predicate lowering".to_string()) })?; let mut terms = prior_false.clone(); - terms.push(branch_condition.clone()); + terms.push(procedural_truth_condition(branch_condition.clone())); let condition = combine_expr_condition_terms(parent_condition.clone(), terms); conditional_assignments_from_statement_or_null( branch, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index cb344e244..4622f54a1 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -299,6 +299,117 @@ mod tests { ); } + #[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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index b8a82ba7b..91dd612f2 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2489,6 +2489,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#" From 857395b238f73a0ecec3669204258b281c046ca1 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 03:43:43 +0900 Subject: [PATCH 06/55] fix(sv-frontend): preserve overlapping comb writes --- crates/celox-sv-analyzer/src/ast.rs | 356 ++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 73 ++++ .../systemverilog/review_regressions.rs | 65 ++++ 3 files changed, 417 insertions(+), 77 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index fef24c14b..9ec70d029 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -2225,7 +2225,7 @@ impl FfEvent { pub struct ConditionalAssignment { condition: Option, assignment: Assignment, - fills_exhaustive_fallback: bool, + exhaustive_fallback_start: Option, } impl ConditionalAssignment { @@ -2233,7 +2233,7 @@ impl ConditionalAssignment { Self { condition, assignment, - fills_exhaustive_fallback: false, + exhaustive_fallback_start: None, } } @@ -7260,7 +7260,7 @@ fn comb_assignments_from_guarded( } let previous = *groups[group].last()?; let separated_by_overlap = guarded[previous + 1..index].iter().any(|assignment| { - lvalues_overlap(assignment.assignment().lhs_value(), target) + lvalues_overlap(assignment.assignment().lhs_value(), target, const_env) && assignment.assignment().lhs_value() != target }); (!separated_by_overlap).then_some(group) @@ -7283,7 +7283,7 @@ fn comb_assignments_from_guarded( { continue; } - let initial = overlapping_whole_value_before(&guarded, indices[0], target); + let initial = overlapping_value_before(&guarded, indices[0], target, const_env); substitute_intermediate_comb_value_reads( &mut guarded, indices, @@ -7313,27 +7313,28 @@ fn comb_assignments_from_guarded( // priority. A target-specific exhaustive fallback fills the previous // value in the branch chain instead of becoming globally // unconditional. - let mut current = overlapping_whole_value_before(&guarded, indices[0], &target) + let mut current = overlapping_value_before(&guarded, indices[0], &target, const_env) .unwrap_or_else(comb_previous_value_placeholder); - for index in &indices { + for (position, index) in indices.iter().enumerate() { let write = &guarded[*index]; let value = write.assignment().rhs().clone(); - current = if write.fills_exhaustive_fallback { - substitute_comb_previous_value(current, &value) - } else { - match write.condition() { - None => value, - Some(condition) => Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(value), - else_expr: Box::new(current), - }, + 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) + { + branch_value = fold_conditional_assignment_over(branch_value, &guarded[prior]); } + branch_value + } else { + fold_conditional_assignment_over(current, write) }; } let rhs = current; if expr_contains_comb_previous_value(&rhs) - || expr_references_overlapping_lvalue(&rhs, &target) + || expr_references_overlapping_lvalue(&rhs, &target, const_env) { return Err(AnalyzerError::Unsupported( "latch inference inside always_comb".to_string(), @@ -7344,6 +7345,18 @@ fn comb_assignments_from_guarded( Ok(slots.into_iter().flatten().collect()) } +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), + }, + } +} + /// 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 @@ -7360,6 +7373,7 @@ fn substitute_intermediate_comb_value_reads( 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::new(); for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { let is_target_write = indices.get(write_index) == Some(&index); @@ -7389,27 +7403,26 @@ fn substitute_intermediate_comb_value_reads( write_index += 1; let write = &*guarded_assignment; let value = write.assignment().rhs().clone(); - if write.fills_exhaustive_fallback { - established = substitute_comb_previous_value(established, &value); + if let Some(chain_start) = write.exhaustive_fallback_start { + established = value; + for (prior_index, prior_write) in &prior_target_writes { + if *prior_index >= chain_start { + established = fold_conditional_assignment_over(established, prior_write); + } + } initialized = true; } else { - established = match write.condition() { - None => { - initialized = true; - value - } - Some(condition) => Expr::Mux { - condition: Box::new(condition.clone()), - then_expr: Box::new(value), - else_expr: Box::new(established), - }, - }; + if write.condition().is_none() { + initialized = true; + } + established = fold_conditional_assignment_over(established, write); } + prior_target_writes.push((index, write.clone())); } Ok(()) } -fn lvalues_overlap(left: &LValue, right: &LValue) -> bool { +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, .. }) @@ -7427,54 +7440,215 @@ fn lvalues_overlap(left: &LValue, right: &LValue) -> bool { lsb: right_lsb, .. }, - ) => left_name == right_name && left_msb == right_msb && left_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, + } + } } } -fn overlapping_whole_value_before( +fn overlapping_value_before( guarded: &[ConditionalAssignment], before: usize, target: &LValue, + const_env: &HashMap, ) -> Option { - let LValue::Select { - name, - msb, - lsb, - signed, - .. - } = target - else { + if !matches!(target, LValue::Select { .. }) { return None; - }; + } let mut current = comb_previous_value_placeholder(); let mut initialized = false; - for write in &guarded[..before] { - if !matches!(write.assignment().lhs_value(), LValue::Ident(whole) if whole == name) { + 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(), + const_env, + ) else { + states_before.push((current.clone(), initialized)); continue; - } - let value = write.assignment().rhs().clone(); - if write.fills_exhaustive_fallback { - current = substitute_comb_previous_value(current, &value); - initialized = true; + }; + 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(), + const_env, + ) 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(), + const_env, + ) 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 = true; - value + initialized |= covers_target; + updated } Some(condition) => Expr::Mux { condition: Box::new(condition.clone()), - then_expr: Box::new(value), + then_expr: Box::new(updated), else_expr: Box::new(current), }, }; } + states_before.push((current.clone(), initialized)); + } + initialized.then_some(current) +} + +fn selected_value_after_write( + current: &Expr, + target: &LValue, + write_target: &LValue, + write_value: &Expr, + const_env: &HashMap, +) -> Option<(Expr, bool)> { + 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(|| { + ( + Expr::Select { + expr: Box::new(write_value.clone()), + msb: target_msb_expr.clone(), + lsb: target_lsb_expr.clone(), + signed: *signed, + }, + true, + ) + }), + LValue::Select { + name: write_name, + msb: write_msb_expr, + lsb: write_lsb_expr, + .. + } => { + if write_name != target_name { + return None; + } + 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 = eval_ast_const_expr(write_msb_expr, const_env)?; + let write_lsb = eval_ast_const_expr(write_lsb_expr, const_env)?; + 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, + )) + } } - initialized.then_some(Expr::Select { - expr: Box::new(current), - msb: msb.clone(), - lsb: lsb.clone(), - signed: *signed, +} + +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, }) } @@ -7484,12 +7658,6 @@ fn comb_previous_value_placeholder() -> Expr { Expr::Ident(COMB_PREVIOUS_VALUE.to_string()) } -fn substitute_comb_previous_value(expr: Expr, value: &Expr) -> Expr { - let mut env = HashMap::default(); - env.insert(COMB_PREVIOUS_VALUE.to_string(), value.clone()); - substitute_expr_idents(expr, &env) -} - fn expr_contains_comb_previous_value(expr: &Expr) -> bool { match expr { Expr::Ident(name) => name == COMB_PREVIOUS_VALUE, @@ -7741,7 +7909,11 @@ fn expr_references_lvalue(expr: &Expr, target: &LValue) -> bool { } } -fn expr_references_overlapping_lvalue(expr: &Expr, target: &LValue) -> bool { +fn expr_references_overlapping_lvalue( + expr: &Expr, + target: &LValue, + const_env: &HashMap, +) -> bool { if expr_matches_lvalue(expr, target) { return true; } @@ -7753,35 +7925,60 @@ fn expr_references_overlapping_lvalue(expr: &Expr, target: &LValue) -> bool { } => name == target_name, }, Expr::Literal(_) => false, - Expr::Select { expr, .. } => { - if matches!((&**expr, target), (Expr::Ident(_), LValue::Select { .. })) { - false + Expr::Select { + expr: selected, + msb, + lsb, + .. + } => { + 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(expr, target) + expr_references_overlapping_lvalue(selected, target, const_env) } } Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { - expr_references_overlapping_lvalue(expr, target) + expr_references_overlapping_lvalue(expr, target, const_env) } Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts .iter() - .any(|part| expr_references_overlapping_lvalue(part, target)), + .any(|part| expr_references_overlapping_lvalue(part, target, const_env)), Expr::Binary { left, right, .. } => { - expr_references_overlapping_lvalue(left, target) - || expr_references_overlapping_lvalue(right, target) + 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) - || expr_references_overlapping_lvalue(then_expr, target) - || expr_references_overlapping_lvalue(else_expr, target) + 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)), + .any(|arg| expr_references_overlapping_lvalue(arg, target, const_env)), } } @@ -8347,6 +8544,7 @@ fn conditional_assignments_from_conditional_statement( 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(|| { @@ -8424,6 +8622,7 @@ fn conditional_assignments_from_conditional_statement( mark_exhaustive_fallback( &mut assignments[branch_start..], &definitely_assigned_branches, + chain_start, ); } } @@ -8449,6 +8648,7 @@ fn conditional_assignments_from_case_statement( 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(), @@ -8551,6 +8751,7 @@ fn conditional_assignments_from_case_statement( mark_exhaustive_fallback( &mut assignments[branch_start..], &definitely_assigned_branches, + chain_start, ); } } @@ -8560,6 +8761,7 @@ fn conditional_assignments_from_case_statement( fn mark_exhaustive_fallback( fallback_assignments: &mut [ConditionalAssignment], branch_targets: &[Vec], + chain_start: usize, ) { let mut marked_targets = Vec::new(); for assignment in fallback_assignments { @@ -8570,7 +8772,7 @@ fn mark_exhaustive_fallback( && !marked_targets.contains(target) { marked_targets.push(target.clone()); - assignment.fills_exhaustive_fallback = true; + assignment.exhaustive_fallback_start = Some(chain_start); } } } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 4622f54a1..4233d890b 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -271,6 +271,34 @@ mod tests { ); } + #[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 substitutes_reads_of_selected_comb_targets() { let ir = analyze_source( @@ -456,6 +484,33 @@ mod tests { assert_eq!(ir.modules()[0].comb_processes()[0].assignments().len(), 2); } + #[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( @@ -492,6 +547,24 @@ mod tests { 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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 91dd612f2..ee5adc247 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2267,6 +2267,71 @@ fn merges_overlapping_writes_inside_always_comb() { 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 2690f96e985a80ec00cb4e7f370adcf00b685ec8 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 04:59:50 +0900 Subject: [PATCH 07/55] fix(sv-frontend): preserve guarded comb state --- crates/celox-sv-analyzer/src/ast.rs | 129 +++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 78 +++++++++++ .../systemverilog/review_regressions.rs | 84 ++++++++++++ 3 files changed, 274 insertions(+), 17 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 9ec70d029..d20991a5a 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -682,7 +682,12 @@ fn size_system_function_expr_type( if !first_dimension_only { return expr_type_from_type(&r#type, const_env); } - let range = r#type.packed_ranges().first()?; + let Some(range) = r#type.packed_ranges().first() else { + return Some(ExprType { + width: 1, + signed: r#type.is_signed(), + }); + }; let left = eval_ast_const_expr(range.left(), const_env)?; let right = eval_ast_const_expr(range.right(), const_env)?; let width = usize::try_from(left.abs_diff(right)).ok()?.checked_add(1)?; @@ -2226,6 +2231,7 @@ pub struct ConditionalAssignment { condition: Option, assignment: Assignment, exhaustive_fallback_start: Option, + condition_epoch: Option, } impl ConditionalAssignment { @@ -2234,6 +2240,7 @@ impl ConditionalAssignment { condition, assignment, exhaustive_fallback_start: None, + condition_epoch: None, } } @@ -7228,8 +7235,7 @@ fn comb_process_from_always_construct( packed_dimensions, &mut guarded_assignments, )?; - let assignments = - comb_assignments_from_guarded(guarded_assignments, &packed_dimensions.const_env)?; + let assignments = comb_assignments_from_guarded(guarded_assignments, packed_dimensions)?; Ok((!assignments.is_empty()) .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) } @@ -7244,8 +7250,9 @@ fn comb_process_from_always_construct( /// previous value, which infers a latch, and is rejected. fn comb_assignments_from_guarded( mut guarded: Vec, - const_env: &HashMap, + packed_dimensions: &PackedDimensions, ) -> Result, AnalyzerError> { + let const_env = &packed_dimensions.const_env; let mut targets: Vec = Vec::new(); let mut groups: Vec> = Vec::new(); for (index, conditional) in guarded.iter().enumerate() { @@ -7283,7 +7290,7 @@ fn comb_assignments_from_guarded( { continue; } - let initial = overlapping_value_before(&guarded, indices[0], target, const_env); + let initial = overlapping_value_before(&guarded, indices[0], target, packed_dimensions); substitute_intermediate_comb_value_reads( &mut guarded, indices, @@ -7313,8 +7320,9 @@ fn comb_assignments_from_guarded( // priority. A target-specific exhaustive fallback fills the previous // value in the branch chain instead of becoming globally // unconditional. - let mut current = overlapping_value_before(&guarded, indices[0], &target, const_env) - .unwrap_or_else(comb_previous_value_placeholder); + 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(); @@ -7334,7 +7342,8 @@ fn comb_assignments_from_guarded( } let rhs = current; if expr_contains_comb_previous_value(&rhs) - || expr_references_overlapping_lvalue(&rhs, &target, const_env) + || (!has_established_initial + && expr_references_overlapping_lvalue(&rhs, &target, const_env)) { return Err(AnalyzerError::Unsupported( "latch inference inside always_comb".to_string(), @@ -7369,22 +7378,45 @@ fn substitute_intermediate_comb_value_reads( const_env: &HashMap, ) -> Result<(), AnalyzerError> { let first = *indices.first().expect("group is non-empty"); - let last = *indices.last().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::new(); + let mut frozen_conditions: HashMap = HashMap::default(); + let mut path_values: HashMap = HashMap::default(); - for (index, guarded_assignment) in guarded.iter_mut().enumerate().take(last + 1).skip(first) { + for (index, guarded_assignment) in guarded.iter_mut().enumerate().skip(first) { let is_target_write = indices.get(write_index) == Some(&index); - if initialized { - guarded_assignment.condition = guarded_assignment.condition.take().map(|condition| { + if let Some(condition) = guarded_assignment.condition.take() { + let condition = if let Some(epoch) = guarded_assignment.condition_epoch { + if let Some(frozen) = frozen_conditions.get(&epoch) { + frozen.clone() + } else { + let frozen = if initialized { + substitute_expr_lvalue(condition, target, &established, const_env) + } else { + condition + }; + frozen_conditions.insert(epoch, frozen.clone()); + frozen + } + } else if initialized { substitute_expr_lvalue(condition, target, &established, const_env) - }); + } else { + condition + }; + guarded_assignment.condition = Some(condition); + } + + let path_value = guarded_assignment + .condition_epoch + .and_then(|epoch| path_values.get(&epoch)); + if initialized || path_value.is_some() { + let value = path_value.unwrap_or(&established); let assignment = guarded_assignment.assignment.clone(); guarded_assignment.assignment = Assignment::new( assignment.lhs_value().clone(), - substitute_expr_lvalue(assignment.rhs, target, &established, const_env), + substitute_expr_lvalue(assignment.rhs, target, value, const_env), ); } else if !is_target_write && (guarded_assignment @@ -7417,6 +7449,9 @@ fn substitute_intermediate_comb_value_reads( } established = fold_conditional_assignment_over(established, write); } + if let Some(epoch) = write.condition_epoch { + path_values.insert(epoch, write.assignment().rhs().clone()); + } prior_target_writes.push((index, write.clone())); } Ok(()) @@ -7464,11 +7499,13 @@ fn overlapping_value_before( guarded: &[ConditionalAssignment], before: usize, target: &LValue, - const_env: &HashMap, + packed_dimensions: &PackedDimensions, ) -> Option { - if !matches!(target, LValue::Select { .. }) { - return None; + 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 const_env = &packed_dimensions.const_env; let mut current = comb_previous_value_placeholder(); let mut initialized = false; let mut states_before = vec![(current.clone(), initialized)]; @@ -7537,6 +7574,43 @@ fn overlapping_value_before( initialized.then_some(current) } +fn whole_packed_lvalue(name: &str, packed_dimensions: &PackedDimensions) -> Option { + let dimensions = packed_dimensions.get(name)?; + if !dimensions.unpacked.is_empty() { + return None; + } + let (msb, lsb) = if 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()) + .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 selected_value_after_write( current: &Expr, target: &LValue, @@ -8556,6 +8630,7 @@ fn conditional_assignments_from_conditional_statement( 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, @@ -8565,6 +8640,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; + mark_condition_epoch(assignments, then_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( &stmt.nodes.3, syntax_tree, @@ -8581,6 +8657,7 @@ fn conditional_assignments_from_conditional_statement( 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, @@ -8590,6 +8667,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; + mark_condition_epoch(assignments, branch_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8613,6 +8691,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; + mark_condition_epoch(assignments, branch_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8707,6 +8786,7 @@ fn conditional_assignments_from_case_statement( 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, @@ -8716,6 +8796,7 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; + mark_condition_epoch(assignments, branch_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8742,6 +8823,7 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; + mark_condition_epoch(assignments, branch_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8758,6 +8840,19 @@ fn conditional_assignments_from_case_statement( Ok(()) } +fn mark_condition_epoch(assignments: &mut [ConditionalAssignment], start: usize) { + let epoch = assignments + .iter() + .filter_map(|assignment| assignment.condition_epoch) + .max() + .map_or(0, |epoch| epoch + 1); + for assignment in &mut assignments[start..] { + if assignment.condition.is_some() && assignment.condition_epoch.is_none() { + assignment.condition_epoch = Some(epoch); + } + } +} + fn mark_exhaustive_fallback( fallback_assignments: &mut [ConditionalAssignment], branch_targets: &[Vec], diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 4233d890b..ac6692f1a 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -484,6 +484,71 @@ mod tests { 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 preserves_prior_partially_overlapping_selected_writes() { let ir = analyze_source( @@ -597,6 +662,19 @@ mod tests { assert_eq!(ir.modules()[0].parameters()[1].resolved_value(), Some(7)); } + #[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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index ee5adc247..845bf82cf 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2332,6 +2332,90 @@ fn later_exhaustive_comb_chain_overrides_an_earlier_chain() { 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 4c4033a531021b7e1f5df090e69e703694ede7b1 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 05:51:50 +0900 Subject: [PATCH 08/55] fix(sv-frontend): preserve nested comb semantics --- crates/celox-sv-analyzer/src/ast.rs | 309 ++++++++++++++---- .../systemverilog/review_regressions.rs | 135 ++++++++ 2 files changed, 380 insertions(+), 64 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index d20991a5a..615bb54bb 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -150,7 +150,18 @@ impl Module { let node = node.into(); let name = module_name_from_node(node.clone(), syntax_tree)?; let type_aliases = type_aliases_from_module_node(node.clone(), syntax_tree)?; - let mut parameters = parameters_from_module_node(node.clone(), syntax_tree, &type_aliases)?; + 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, + applicable_parameter_overrides, + )?; let mut parameter_names = HashSet::default(); if let Some(parameter) = parameters .iter() @@ -634,7 +645,9 @@ fn cast_target_type( } } sv_parser::CastingType::ConstantPrimary(primary) => { - if let Some(r#type) = size_system_function_expr_type(primary, syntax_tree, const_env) { + 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)?; @@ -657,6 +670,7 @@ fn size_system_function_expr_type( primary: &sv_parser::ConstantPrimary, syntax_tree: &SyntaxTree, const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { let sv_parser::ConstantPrimary::ConstantFunctionCall(call) = primary else { return None; @@ -664,21 +678,42 @@ fn size_system_function_expr_type( let sv_parser::SubroutineCall::SystemTfCall(system_call) = &call.nodes.0.nodes.0 else { return None; }; - let sv_parser::SystemTfCall::ArgDataType(call) = &**system_call 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(RefNode::DataType(data_type), syntax_tree), + }?; + (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()?; + let ConstExpr::Ident(alias) = const_expr_from_expr(argument, syntax_tree)? else { + return None; + }; + (name, type_aliases.get(&alias)?.clone()) + } + sv_parser::SystemTfCall::ArgOptionl(_) => return None, }; - let name = syntax_tree.get_str(&call.nodes.0.nodes.0)?; // $bits covers every packed dimension; $size covers only the first. let first_dimension_only = name == "$size"; if name != "$bits" && !first_dimension_only { return None; } - let data_type = unwrap_node!( - RefNode::SystemTfCallArgDataType(call), - DataTypeVector, - DataTypeAtom - )?; - let r#type = type_from_ref_node(data_type.clone(), syntax_tree)?; if !first_dimension_only { return expr_type_from_type(&r#type, const_env); } @@ -2231,7 +2266,10 @@ pub struct ConditionalAssignment { condition: Option, assignment: Assignment, exhaustive_fallback_start: Option, - condition_epoch: 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 { @@ -2240,7 +2278,8 @@ impl ConditionalAssignment { condition, assignment, exhaustive_fallback_start: None, - condition_epoch: None, + guard_boundary: None, + path_epochs: Vec::new(), } } @@ -2418,6 +2457,7 @@ fn parameters_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, type_aliases: &HashMap, + parameter_overrides: &HashMap, ) -> Result, AnalyzerError> { let base_const_env = HashMap::default(); let mut parameters = Vec::new(); @@ -2429,6 +2469,7 @@ fn parameters_from_module_node( false, &base_const_env, type_aliases, + parameter_overrides, )?; let mut local_parameters = Vec::new(); for child in parameter_port_list { @@ -2440,6 +2481,7 @@ fn parameters_from_module_node( true, &base_const_env, type_aliases, + parameter_overrides, )?; } } @@ -2465,6 +2507,7 @@ fn parameters_from_module_node( true, &base_const_env, type_aliases, + parameter_overrides, )?, sv_parser::PackageOrGenerateItemDeclaration::ParameterDeclaration(parameter) => { parameters_from_ref_node( @@ -2474,6 +2517,7 @@ fn parameters_from_module_node( false, &base_const_env, type_aliases, + parameter_overrides, )? } _ => {} @@ -3012,6 +3056,7 @@ fn parameters_from_ref_node( is_local: bool, base_const_env: &HashMap, type_aliases: &HashMap, + parameter_overrides: &HashMap, ) -> Result<(), AnalyzerError> { if node.clone().into_iter().any(|child| { matches!( @@ -3054,6 +3099,13 @@ fn parameters_from_ref_node( }); 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, @@ -6204,6 +6256,7 @@ fn generate_block_direct_local_parameter_names( true, &HashMap::default(), &HashMap::default(), + &HashMap::default(), ) .is_ok() { @@ -6400,6 +6453,7 @@ fn add_localparams_from_generate_item_with_literals( true, const_env, &HashMap::default(), + &HashMap::default(), ) .is_err() { @@ -7296,7 +7350,7 @@ fn comb_assignments_from_guarded( indices, target, initial, - const_env, + packed_dimensions, )?; } @@ -7375,33 +7429,49 @@ fn substitute_intermediate_comb_value_reads( indices: &[usize], target: &LValue, initial: Option, - const_env: &HashMap, + packed_dimensions: &PackedDimensions, ) -> Result<(), 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::new(); - let mut frozen_conditions: HashMap = HashMap::default(); + let mut guard_values: HashMap = HashMap::default(); let mut path_values: HashMap = HashMap::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) + }); for (index, guarded_assignment) in guarded.iter_mut().enumerate().skip(first) { let is_target_write = indices.get(write_index) == Some(&index); if let Some(condition) = guarded_assignment.condition.take() { - let condition = if let Some(epoch) = guarded_assignment.condition_epoch { - if let Some(frozen) = frozen_conditions.get(&epoch) { - frozen.clone() + 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 { - let frozen = if initialized { - substitute_expr_lvalue(condition, target, &established, const_env) - } else { - condition - }; - frozen_conditions.insert(epoch, frozen.clone()); - frozen + condition } } else if initialized { - substitute_expr_lvalue(condition, target, &established, const_env) + substitute_comb_value_reads( + condition, + target, + &established, + whole_established.as_ref(), + packed_dimensions, + ) } else { condition }; @@ -7409,14 +7479,21 @@ fn substitute_intermediate_comb_value_reads( } let path_value = guarded_assignment - .condition_epoch - .and_then(|epoch| path_values.get(&epoch)); + .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(); guarded_assignment.assignment = Assignment::new( assignment.lhs_value().clone(), - substitute_expr_lvalue(assignment.rhs, target, value, const_env), + substitute_comb_value_reads( + assignment.rhs, + target, + value, + whole_established.as_ref(), + packed_dimensions, + ), ); } else if !is_target_write && (guarded_assignment @@ -7435,6 +7512,25 @@ fn substitute_intermediate_comb_value_reads( write_index += 1; let write = &*guarded_assignment; let value = write.assignment().rhs().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.const_env, + ) + { + 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; for (prior_index, prior_write) in &prior_target_writes { @@ -7449,14 +7545,38 @@ fn substitute_intermediate_comb_value_reads( } established = fold_conditional_assignment_over(established, write); } - if let Some(epoch) = write.condition_epoch { - path_values.insert(epoch, write.assignment().rhs().clone()); + for (depth, epoch) in write.path_epochs.iter().enumerate() { + if depth == 0 { + path_values.insert(*epoch, write.assignment().rhs().clone()); + } else { + let current = path_values + .get(epoch) + .cloned() + .unwrap_or_else(|| established.clone()); + path_values.insert(*epoch, fold_conditional_assignment_over(current, write)); + } } prior_target_writes.push((index, write.clone())); } Ok(()) } +fn substitute_comb_value_reads( + expr: Expr, + target: &LValue, + value: &Expr, + whole_value: Option<&Expr>, + packed_dimensions: &PackedDimensions, +) -> Expr { + 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) + } +} + fn lvalues_overlap(left: &LValue, right: &LValue, const_env: &HashMap) -> bool { match (left, right) { (LValue::Ident(left), LValue::Ident(right)) => left == right, @@ -7762,12 +7882,13 @@ fn substitute_expr_lvalue( expr: Expr, target: &LValue, value: &Expr, - const_env: &HashMap, + packed_dimensions: &PackedDimensions, ) -> Expr { if expr_matches_lvalue(&expr, target) { return value.clone(); } - if let Some(replacement) = substitute_overlapping_selected_read(&expr, target, value, const_env) + if let Some(replacement) = + substitute_overlapping_selected_read(&expr, target, value, packed_dimensions) { return replacement; } @@ -7779,7 +7900,12 @@ fn substitute_expr_lvalue( lsb, signed, } => Expr::Select { - expr: Box::new(substitute_expr_lvalue(*expr, target, value, const_env)), + expr: Box::new(substitute_expr_lvalue( + *expr, + target, + value, + packed_dimensions, + )), msb, lsb, signed, @@ -7787,14 +7913,14 @@ fn substitute_expr_lvalue( Expr::Concat(parts) => Expr::Concat( parts .into_iter() - .map(|part| substitute_expr_lvalue(part, target, value, const_env)) + .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, const_env)) + .map(|part| substitute_expr_lvalue(part, target, value, packed_dimensions)) .collect(), }, Expr::Resize { @@ -7802,33 +7928,68 @@ fn substitute_expr_lvalue( width, signed, } => Expr::Resize { - expr: Box::new(substitute_expr_lvalue(*expr, target, value, const_env)), + 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, const_env)), + 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, const_env)), + left: Box::new(substitute_expr_lvalue( + *left, + target, + value, + packed_dimensions, + )), op, - right: Box::new(substitute_expr_lvalue(*right, target, value, const_env)), + 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, const_env)), - then_expr: Box::new(substitute_expr_lvalue(*then_expr, target, value, const_env)), - else_expr: Box::new(substitute_expr_lvalue(*else_expr, target, value, const_env)), + 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, const_env)) + .map(|arg| substitute_expr_lvalue(arg, target, value, packed_dimensions)) .collect(), }, } @@ -7838,8 +7999,32 @@ fn substitute_overlapping_selected_read( expr: &Expr, target: &LValue, value: &Expr, - const_env: &HashMap, + 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; + }; + 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, + const_env, + ) + .map(|(value, _)| value); + } let Expr::Select { expr: read_base, msb: read_msb, @@ -7852,15 +8037,6 @@ fn substitute_overlapping_selected_read( let Expr::Ident(read_name) = &**read_base else { return None; }; - let LValue::Select { - name: target_name, - msb: target_msb, - lsb: target_lsb, - .. - } = target - else { - return None; - }; if read_name != target_name { return None; } @@ -8640,7 +8816,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - mark_condition_epoch(assignments, then_start); + mark_condition_context(assignments, then_start, chain_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( &stmt.nodes.3, syntax_tree, @@ -8667,7 +8843,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - mark_condition_epoch(assignments, branch_start); + mark_condition_context(assignments, branch_start, chain_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8691,7 +8867,7 @@ fn conditional_assignments_from_conditional_statement( packed_dimensions, assignments, )?; - mark_condition_epoch(assignments, branch_start); + mark_condition_context(assignments, branch_start, chain_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8796,7 +8972,7 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; - mark_condition_epoch(assignments, branch_start); + mark_condition_context(assignments, branch_start, chain_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8823,7 +8999,7 @@ fn conditional_assignments_from_case_statement( packed_dimensions, assignments, )?; - mark_condition_epoch(assignments, branch_start); + mark_condition_context(assignments, branch_start, chain_start); definitely_assigned_branches.push(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, @@ -8840,15 +9016,20 @@ fn conditional_assignments_from_case_statement( Ok(()) } -fn mark_condition_epoch(assignments: &mut [ConditionalAssignment], start: usize) { +fn mark_condition_context( + assignments: &mut [ConditionalAssignment], + start: usize, + guard_boundary: usize, +) { let epoch = assignments .iter() - .filter_map(|assignment| assignment.condition_epoch) + .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.condition_epoch.is_none() { - assignment.condition_epoch = Some(epoch); + if assignment.condition.is_some() { + assignment.guard_boundary.get_or_insert(guard_boundary); + assignment.path_epochs.push(epoch); } } } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 845bf82cf..1520c145e 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2416,6 +2416,141 @@ fn freezes_comb_branch_guards_before_predicate_writes() { 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From e91c4ab9c3a56b0332d199309067464846120ae5 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 12:54:02 +0900 Subject: [PATCH 09/55] fix(sv-frontend): preserve guarded assignment semantics --- crates/celox-sv-analyzer/src/ast.rs | 450 +++++++++++++++++- .../systemverilog/review_regressions.rs | 78 +++ 2 files changed, 503 insertions(+), 25 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 615bb54bb..8559fd5c3 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -160,6 +160,7 @@ impl Module { node.clone(), syntax_tree, &type_aliases, + &HashMap::default(), applicable_parameter_overrides, )?; let mut parameter_names = HashSet::default(); @@ -189,6 +190,20 @@ impl Module { 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. + parameters = parameters_from_module_node( + node.clone(), + syntax_tree, + &type_aliases, + &const_env, + applicable_parameter_overrides, + )?; + if name == override_module_name { + apply_parameter_overrides(&mut parameters, parameter_overrides)?; + } extend_const_env_with_parameters(&mut const_env, ¶meters); reject_silently_ignored_constructs(node.clone(), syntax_tree, &const_env, &type_aliases)?; let ports = ports_from_module_node(node.clone(), syntax_tree)?; @@ -2457,9 +2472,9 @@ fn parameters_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, type_aliases: &HashMap, + base_const_env: &HashMap, parameter_overrides: &HashMap, ) -> Result, AnalyzerError> { - let base_const_env = HashMap::default(); let mut parameters = Vec::new(); if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { parameters_from_ref_node( @@ -2467,7 +2482,7 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, false, - &base_const_env, + base_const_env, type_aliases, parameter_overrides, )?; @@ -2479,7 +2494,7 @@ fn parameters_from_module_node( syntax_tree, &mut local_parameters, true, - &base_const_env, + base_const_env, type_aliases, parameter_overrides, )?; @@ -2505,7 +2520,7 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, true, - &base_const_env, + base_const_env, type_aliases, parameter_overrides, )?, @@ -2515,7 +2530,7 @@ fn parameters_from_module_node( syntax_tree, &mut parameters, false, - &base_const_env, + base_const_env, type_aliases, parameter_overrides, )? @@ -7306,6 +7321,7 @@ fn comb_assignments_from_guarded( mut guarded: Vec, packed_dimensions: &PackedDimensions, ) -> Result, AnalyzerError> { + normalize_mixed_whole_selected_comb_writes(&mut guarded, packed_dimensions); let const_env = &packed_dimensions.const_env; let mut targets: Vec = Vec::new(); let mut groups: Vec> = Vec::new(); @@ -7382,11 +7398,9 @@ fn comb_assignments_from_guarded( 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) - { + 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]); } branch_value @@ -7408,6 +7422,66 @@ fn comb_assignments_from_guarded( Ok(slots.into_iter().flatten().collect()) } +fn normalize_mixed_whole_selected_comb_writes( + guarded: &mut [ConditionalAssignment], + packed_dimensions: &PackedDimensions, +) { + let mut whole_names = HashSet::default(); + let mut selected_names = HashSet::default(); + for write in guarded.iter() { + match write.assignment().lhs_value() { + LValue::Ident(name) if write.condition().is_some() => { + whole_names.insert(name.clone()); + } + LValue::Ident(_) => {} + LValue::Select { name, .. } => { + selected_names.insert(name.clone()); + } + } + } + whole_names.retain(|name| selected_names.contains(name)); + for write in guarded.iter_mut() { + let target = write.assignment().lhs_value().clone(); + let LValue::Select { name, .. } = &target else { + continue; + }; + if !whole_names.contains(name) { + continue; + } + let Some(whole_target) = whole_packed_lvalue(name, packed_dimensions) else { + continue; + }; + let Some((rhs, _)) = selected_value_after_write( + &Expr::Ident(name.clone()), + &whole_target, + &target, + write.assignment().rhs(), + &packed_dimensions.const_env, + ) else { + continue; + }; + write.assignment = Assignment::new(LValue::Ident(name.clone()), rhs); + } + + // 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; + } + } +} + fn fold_conditional_assignment_over(current: Expr, write: &ConditionalAssignment) -> Expr { let value = write.assignment().rhs().clone(); match write.condition() { @@ -7435,7 +7509,7 @@ fn substitute_intermediate_comb_value_reads( 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::new(); + 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 whole_target = match target { @@ -7534,7 +7608,7 @@ fn substitute_intermediate_comb_value_reads( if let Some(chain_start) = write.exhaustive_fallback_start { established = value; for (prior_index, prior_write) in &prior_target_writes { - if *prior_index >= chain_start { + if *prior_index >= chain_start && prior_write.path_epochs != write.path_epochs { established = fold_conditional_assignment_over(established, prior_write); } } @@ -7568,12 +7642,196 @@ fn substitute_comb_value_reads( whole_value: Option<&Expr>, packed_dimensions: &PackedDimensions, ) -> Expr { - 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) + 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 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 bit = match ( + eval_ast_const_expr(&msb, &packed_dimensions.const_env), + eval_ast_const_expr(&lsb, &packed_dimensions.const_env), + ) { + (Some(msb), Some(lsb)) if msb == lsb => Some(msb), + _ => None, + }; + if let (Some(bit), Expr::Concat(parts)) = (bit, &expr) + && let Ok(bit) = usize::try_from(bit) + { + let mut offset = 0usize; + for part in parts.iter().rev() { + let Some(width) = expr_static_width(part, packed_dimensions) else { + break; + }; + if bit < offset.saturating_add(width) { + let selected = bit - 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 = offset.saturating_add(width); + } + } + 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, + 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, + } +} + +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), + Expr::Literal(literal) => { + typecheck::parse_integral_literal(literal).map(|literal| literal.width) + } + 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) + } + 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) + } + 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) + } + } + 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 { .. } => None, } } @@ -8671,6 +8929,11 @@ fn conditional_assignments_from_statement( "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), @@ -8690,6 +8953,11 @@ fn conditional_assignments_from_statement( .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), @@ -8785,6 +9053,69 @@ fn conditional_assignments_from_statement( Ok(()) } +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(&rhs, &identifier_signedness, &HashMap::default()) + else { + return rhs; + }; + if source_signed == target_type.signed + && expr_static_width(&rhs, packed_dimensions) == Some(target_type.width) + { + return rhs; + } + 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, + } +} + +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() + .try_fold(1usize, |width, dimension| { + let dimension_width = + eval_ast_const_expr(&dimension.width, &packed_dimensions.const_env)?; + width.checked_mul(usize::try_from(dimension_width).ok()?) + })?; + Some(ExprType { + width: width.max(1), + signed: dimensions.signed, + }) + } + 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 conditional_assignments_from_conditional_statement( stmt: &sv_parser::ConditionalStatement, parent_condition: Option, @@ -8878,6 +9209,7 @@ fn conditional_assignments_from_conditional_statement( &mut assignments[branch_start..], &definitely_assigned_branches, chain_start, + packed_dimensions, ); } } @@ -9010,6 +9342,7 @@ fn conditional_assignments_from_case_statement( &mut assignments[branch_start..], &definitely_assigned_branches, chain_start, + packed_dimensions, ); } } @@ -9038,13 +9371,14 @@ fn mark_exhaustive_fallback( fallback_assignments: &mut [ConditionalAssignment], branch_targets: &[Vec], chain_start: usize, + packed_dimensions: &PackedDimensions, ) { let mut marked_targets = Vec::new(); for assignment in fallback_assignments { let target = assignment.assignment().lhs_value(); if branch_targets .iter() - .all(|targets| targets.contains(target)) + .all(|targets| lvalue_is_covered_by(target, targets, packed_dimensions)) && !marked_targets.contains(target) { marked_targets.push(target.clone()); @@ -9120,7 +9454,7 @@ fn definitely_assigned_comb_targets( syntax_tree, packed_dimensions, )); - intersect_lvalue_sets(branches) + intersect_lvalue_sets(branches, packed_dimensions) } sv_parser::StatementItem::CaseStatement(case) => { let sv_parser::CaseStatement::Normal(case) = &**case else { @@ -9145,7 +9479,7 @@ fn definitely_assigned_comb_targets( }) .collect::>(); if has_default { - intersect_lvalue_sets(branches) + intersect_lvalue_sets(branches, packed_dimensions) } else { Vec::new() } @@ -9154,12 +9488,78 @@ fn definitely_assigned_comb_targets( } } -fn intersect_lvalue_sets(mut sets: Vec>) -> Vec { - let Some(mut intersection) = sets.pop() else { +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 +} + +fn lvalue_is_covered_by( + target: &LValue, + writes: &[LValue], + packed_dimensions: &PackedDimensions, +) -> bool { + if writes.contains(target) { + return true; + } + 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!(); }; - intersection.retain(|target| sets.iter().all(|set| set.contains(target))); - intersection + 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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 1520c145e..e771ddd34 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2551,6 +2551,84 @@ fn resolves_typedefs_in_size_function_cast_targets() { assert_eq!(sim.get(sim.signal("y")), 0xffu8.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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From ed17eb741f735d467844b38a6202a890a0bb9da2 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 13:44:57 +0900 Subject: [PATCH 10/55] fix(sv-frontend): complete assignment type coercion --- crates/celox-sv-analyzer/src/ast.rs | 79 +++++++++++++++++-- .../systemverilog/review_regressions.rs | 67 ++++++++++++++++ 2 files changed, 139 insertions(+), 7 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 8559fd5c3..bd0fa481f 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -724,22 +724,52 @@ fn size_system_function_expr_type( } sv_parser::SystemTfCall::ArgOptionl(_) => return None, }; - // $bits covers every packed dimension; $size covers only the first. + // $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; } if !first_dimension_only { - return expr_type_from_type(&r#type, const_env); + 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 Some(range) = r#type.packed_ranges().first() else { + 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(range.left(), const_env)?; - let right = eval_ast_const_expr(range.right(), const_env)?; + 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 { width: width.max(1), @@ -3090,7 +3120,8 @@ fn parameters_from_ref_node( "unsupported parameter data type".to_string(), )); } - let parameter_width = parameter_declared_width(node.clone(), syntax_tree, parameters); + let parameter_width = + parameter_declared_width(node.clone(), syntax_tree, base_const_env, parameters); let has_declared_type = node.clone().into_iter().any(|child| { matches!( child, @@ -3138,6 +3169,7 @@ fn parameters_from_ref_node( fn parameter_declared_width( node: RefNode<'_>, syntax_tree: &SyntaxTree, + base_const_env: &HashMap, parameters: &[Parameter], ) -> Option { let ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); @@ -3147,7 +3179,22 @@ fn parameter_declared_width( } return unwrap_node!(node, IntegerVectorType).is_some().then_some(1); } - let env = const_env_from_parameters(parameters); + 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); + } + } + env.extend(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)?; @@ -7350,6 +7397,24 @@ fn comb_assignments_from_guarded( } } } + // 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); + } + } // 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. diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index e771ddd34..44cc6e132 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2629,6 +2629,73 @@ fn coerces_each_guarded_rhs_before_building_a_mux() { 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 1bb3b24e9c6323de13179a5e1554db2f5e3f41d8 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 19:17:58 +0900 Subject: [PATCH 11/55] fix(sv-frontend): preserve declared packed coordinates --- crates/celox-sv-analyzer/src/ast.rs | 51 +++++++++++--- .../systemverilog/review_regressions.rs | 69 +++++++++++++++++++ 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index bd0fa481f..336899832 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -3346,8 +3346,11 @@ fn enum_member_constants_from_module_node( let number = eval_ast_const_expr(&value, &eval_env).ok_or_else(|| { AnalyzerError::Unsupported(format!("unresolved enum member `{name}` value")) })?; + 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, @@ -7521,7 +7524,7 @@ fn normalize_mixed_whole_selected_comb_writes( &whole_target, &target, write.assignment().rhs(), - &packed_dimensions.const_env, + packed_dimensions, ) else { continue; }; @@ -7658,7 +7661,7 @@ fn substitute_intermediate_comb_value_reads( whole_target, target, &value, - &packed_dimensions.const_env, + packed_dimensions, ) { whole_established = Some(match write.condition() { @@ -7948,7 +7951,6 @@ fn overlapping_value_before( let selected_target = whole_packed_lvalue(target_name, packed_dimensions)?; return overlapping_value_before(guarded, before, &selected_target, packed_dimensions); } - let const_env = &packed_dimensions.const_env; let mut current = comb_previous_value_placeholder(); let mut initialized = false; let mut states_before = vec![(current.clone(), initialized)]; @@ -7958,7 +7960,7 @@ fn overlapping_value_before( target, write.assignment().lhs_value(), write.assignment().rhs(), - const_env, + packed_dimensions, ) else { states_before.push((current.clone(), initialized)); continue; @@ -7973,7 +7975,7 @@ fn overlapping_value_before( target, write.assignment().lhs_value(), write.assignment().rhs(), - const_env, + packed_dimensions, ) else { states_before.push((current.clone(), initialized)); continue; @@ -7984,7 +7986,7 @@ fn overlapping_value_before( target, prior.assignment().lhs_value(), prior.assignment().rhs(), - const_env, + packed_dimensions, ) else { continue; }; @@ -8059,8 +8061,9 @@ fn selected_value_after_write( target: &LValue, write_target: &LValue, write_value: &Expr, - const_env: &HashMap, + packed_dimensions: &PackedDimensions, ) -> Option<(Expr, bool)> { + let const_env = &packed_dimensions.const_env; let LValue::Select { name: target_name, msb: target_msb_expr, @@ -8073,11 +8076,17 @@ fn selected_value_after_write( }; 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: target_msb_expr.clone(), - lsb: target_lsb_expr.clone(), + msb, + lsb, signed: *signed, }, true, @@ -8154,6 +8163,28 @@ fn selected_value_after_write( } } +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 selected_value_read( value: &Expr, value_msb: i128, @@ -8344,7 +8375,7 @@ fn substitute_overlapping_selected_read( &whole, target, value, - const_env, + packed_dimensions, ) .map(|(value, _)| value); } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 44cc6e132..18cf23e43 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2696,6 +2696,75 @@ fn resolves_parameter_ranges_with_enum_constants() { assert_eq!(sim.get(sim.signal("y")), 0xffu8.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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 77ed440fa021d6eefb251c209fdb90b4d14ef141 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 20:18:23 +0900 Subject: [PATCH 12/55] fix(sv-frontend): finalize procedural assignments before merging --- crates/celox-sv-analyzer/src/ast.rs | 131 ++++++++++++++++-- .../systemverilog/review_regressions.rs | 77 ++++++++++ 2 files changed, 194 insertions(+), 14 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 336899832..50b417288 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -2930,8 +2930,15 @@ fn add_type_alias_from_data_declaration( else { return Ok(()); }; - let Some(r#type) = type_from_ref_node(RefNode::DataType(&declaration.nodes.1), syntax_tree) - else { + let r#type = + type_from_ref_node(RefNode::DataType(&declaration.nodes.1), syntax_tree).or_else(|| { + let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 else { + return None; + }; + let base = r#enum.nodes.1.as_ref()?; + type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, aliases) + }); + let Some(r#type) = r#type else { return Ok(()); }; let r#type = type_with_unpacked_ranges( @@ -3782,6 +3789,7 @@ struct VariableDimensions { packed: Vec, unpacked: Vec, signed: bool, + is_2state: bool, } type VariablePackedDimensions = HashMap; @@ -3835,6 +3843,7 @@ fn packed_dimensions_from_ports_and_signals( 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, }, ); } @@ -3845,6 +3854,7 @@ fn packed_dimensions_from_ports_and_signals( 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, }, ); } @@ -4940,6 +4950,7 @@ fn function_from_declaration( packed: param.packed_dimensions.clone(), unpacked: Vec::new(), signed: param.signed, + is_2state: param.is_2state, }, ) })); @@ -4996,6 +5007,7 @@ fn function_from_declaration( packed: param.packed_dimensions.clone(), unpacked: Vec::new(), signed: param.signed, + is_2state: param.is_2state, }, ) })); @@ -5085,6 +5097,7 @@ fn function_local_packed_dimensions_from_block_item_iter<'a>( 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, }, ) })); @@ -7476,7 +7489,7 @@ fn comb_assignments_from_guarded( fold_conditional_assignment_over(current, write) }; } - let rhs = current; + 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)) @@ -7562,6 +7575,81 @@ fn fold_conditional_assignment_over(current: Expr, write: &ConditionalAssignment } } +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, + } => Expr::Resize { + expr: Box::new(simplify_constant_mux_conditions(*expr, const_env)), + width, + signed, + }, + 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 then_expr = simplify_constant_mux_conditions(*then_expr, const_env); + let else_expr = simplify_constant_mux_conditions(*else_expr, const_env); + match expr_to_const(condition.clone()) + .and_then(|condition| eval_ast_const_expr(&condition, const_env)) + { + Some(0) => else_expr, + Some(_) => then_expr, + None => Expr::Mux { + condition: Box::new(condition), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + }, + } + } + 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, + } +} + /// 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 @@ -9165,20 +9253,35 @@ fn coerce_procedural_assignment_rhs( else { return rhs; }; - if source_signed == target_type.signed + let assigned = if source_signed == target_type.signed && expr_static_width(&rhs, packed_dimensions) == Some(target_type.width) { - return rhs; - } - let assigned = Expr::Resize { - expr: Box::new(rhs), - width: target_type.width, - signed: source_signed, + 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, + } }; - 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 } } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 18cf23e43..4502b8d64 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2765,6 +2765,83 @@ fn types_earlier_enum_members_in_later_initializers() { 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 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From e17c1893f6d3fb3c2c0b87a3ca2c84af2d4a3469 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 21:47:11 +0900 Subject: [PATCH 13/55] fix(sv-frontend): preserve typed procedural values --- crates/celox-sv-analyzer/src/ast.rs | 166 ++++++++++++++---- .../systemverilog/review_regressions.rs | 124 +++++++++++++ 2 files changed, 259 insertions(+), 31 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 50b417288..5555c9211 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -852,10 +852,14 @@ fn constant_cast_const_expr( type_aliases, )?; let operand_type = infer_const_expr_type(&operand, ¶meter_types_from_const_env(const_env))?; - 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); - let literal = typecheck::parse_integral_literal(&operand_literal)?; + let literal = if let ConstExpr::Literal(literal) = &operand { + typecheck::parse_integral_literal(literal)? + } 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 size cast keeps the source expression's signedness when the target // is described by a constant primary; a type cast takes the target's. @@ -864,32 +868,53 @@ fn constant_cast_const_expr( } else { target_type.signed }; - let mut value = literal.value.clone(); - // Widen negative two's-complement sources with sign extension before - // applying the truncating cast. - if literal.signed - && literal.width > 0 - && literal.width < target_type.width - && (literal.value >> (literal.width - 1)) & num_bigint::BigUint::from(1usize) - == num_bigint::BigUint::from(1usize) - { - value |= ((num_bigint::BigUint::from(1usize) << target_type.width) - - num_bigint::BigUint::from(1usize)) - ^ ((num_bigint::BigUint::from(1usize) << literal.width) - - num_bigint::BigUint::from(1usize)); - } - if value.bits() > target_type.width as u64 { - value &= (num_bigint::BigUint::from(1usize) << target_type.width) - - num_bigint::BigUint::from(1usize); - } - Some(ConstExpr::Literal(format!( - "{}'{}d{}", + Some(ConstExpr::Literal(resize_integral_literal_for_cast( + literal, target_type.width, - if signed { "s" } else { "" }, - value + signed, ))) } +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' + } + }) + .collect::(); + format!("{width}'{signing}b{bits}") +} + fn for_loop_variable_lvalue_name( lvalue: &sv_parser::VariableLvalue, syntax_tree: &SyntaxTree, @@ -2935,8 +2960,17 @@ fn add_type_alias_from_data_declaration( let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 else { return None; }; - let base = r#enum.nodes.1.as_ref()?; - type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, aliases) + if let Some(base) = &r#enum.nodes.1 { + 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) + } }); let Some(r#type) = r#type else { return Ok(()); @@ -7509,14 +7543,18 @@ fn normalize_mixed_whole_selected_comb_writes( ) { let mut whole_names = HashSet::default(); let mut 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() => { + LValue::Ident(name) + if write.condition().is_some() || earlier_selected_names.contains(name) => + { whole_names.insert(name.clone()); } LValue::Ident(_) => {} LValue::Select { name, .. } => { selected_names.insert(name.clone()); + earlier_selected_names.insert(name.clone()); } } } @@ -7532,11 +7570,16 @@ fn normalize_mixed_whole_selected_comb_writes( let Some(whole_target) = whole_packed_lvalue(name, packed_dimensions) else { continue; }; + let write_value = coerce_procedural_assignment_rhs( + write.assignment().rhs().clone(), + &target, + packed_dimensions, + ); let Some((rhs, _)) = selected_value_after_write( &Expr::Ident(name.clone()), &whole_target, &target, - write.assignment().rhs(), + &write_value, packed_dimensions, ) else { continue; @@ -9451,6 +9494,9 @@ fn conditional_assignments_from_case_statement( packed_dimensions, ) .ok_or_else(|| AnalyzerError::Unsupported("always_ff case selector lowering".to_string()))?; + let two_state_selector_width = two_state_case_selector_width(&case_expr, packed_dimensions); + let mut covered_selector_values = HashSet::default(); + let mut labels_are_constant = true; let mut branches = Vec::new(); let mut default_branch = None; @@ -9469,6 +9515,25 @@ fn conditional_assignments_from_case_statement( "always_ff case item expression lowering".to_string(), ) })?; + if let Some(width) = two_state_selector_width { + let value = expr_to_const(expr.clone()) + .and_then(|expr| eval_ast_const_expr(&expr, const_env)); + if let Some(value) = value { + let total = 1i128.checked_shl(u32::try_from(width).unwrap_or(u32::MAX)); + if value >= 0 && total.is_some_and(|total| value < total) { + covered_selector_values.insert(value); + } else if expr_static_width(&expr, packed_dimensions) + .is_some_and(|label_width| label_width <= width) + { + covered_selector_values + .insert(coerce_const_parameter_value(value, width, false)); + } else { + labels_are_constant = false; + } + } else { + labels_are_constant = false; + } + } conditions.push(case_item_condition(case_expr.clone(), expr)); } if let Some(condition) = conditions.into_iter().reduce(|left, right| Expr::Binary { @@ -9485,9 +9550,16 @@ fn conditional_assignments_from_case_statement( } } + let complete_two_state_case = two_state_selector_width + .and_then(|width| 1usize.checked_shl(u32::try_from(width).ok()?)) + .is_some_and(|value_count| { + labels_are_constant && covered_selector_values.len() == value_count + }); + let mut prior_false = Vec::new(); let mut definitely_assigned_branches = Vec::new(); - for (branch_condition, branch) in branches { + 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); @@ -9509,6 +9581,18 @@ fn conditional_assignments_from_case_statement( 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), @@ -9548,6 +9632,26 @@ fn conditional_assignments_from_case_statement( Ok(()) } +fn two_state_case_selector_width( + selector: &Expr, + packed_dimensions: &PackedDimensions, +) -> Option { + let name = match selector { + Expr::Ident(name) => name, + Expr::Select { expr, .. } => { + let Expr::Ident(name) = &**expr else { + return None; + }; + name + } + _ => return None, + }; + packed_dimensions + .get(name) + .is_some_and(|dimensions| dimensions.is_2state) + .then(|| expr_static_width(selector, packed_dimensions))? +} + fn mark_condition_context( assignments: &mut [ConditionalAssignment], start: usize, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 4502b8d64..ca3d2bfca 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2842,6 +2842,130 @@ fn registers_enum_types_with_aliased_bases() { assert_eq!(sim.get(sim.signal("y")), 2u8.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 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 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 1f680e93a63c24c6f5f129c0f39cdfe64684aa38 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 22:15:21 +0900 Subject: [PATCH 14/55] fix(sv-frontend): preserve cast and case semantics --- crates/celox-sv-analyzer/src/ast.rs | 125 +++++++++++++----- .../systemverilog/review_regressions.rs | 57 ++++++++ 2 files changed, 148 insertions(+), 34 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 5555c9211..8fcba7054 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -868,11 +868,28 @@ fn constant_cast_const_expr( } else { target_type.signed }; - Some(ConstExpr::Literal(resize_integral_literal_for_cast( - literal, - target_type.width, - 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), + }; + Some(ConstExpr::Literal(resized)) +} + +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( @@ -9494,9 +9511,8 @@ fn conditional_assignments_from_case_statement( packed_dimensions, ) .ok_or_else(|| AnalyzerError::Unsupported("always_ff case selector lowering".to_string()))?; - let two_state_selector_width = two_state_case_selector_width(&case_expr, packed_dimensions); - let mut covered_selector_values = HashSet::default(); - let mut labels_are_constant = true; + let complete_two_state_case = + two_state_case_items_cover_selector(stmt, syntax_tree, const_env, packed_dimensions); let mut branches = Vec::new(); let mut default_branch = None; @@ -9515,25 +9531,6 @@ fn conditional_assignments_from_case_statement( "always_ff case item expression lowering".to_string(), ) })?; - if let Some(width) = two_state_selector_width { - let value = expr_to_const(expr.clone()) - .and_then(|expr| eval_ast_const_expr(&expr, const_env)); - if let Some(value) = value { - let total = 1i128.checked_shl(u32::try_from(width).unwrap_or(u32::MAX)); - if value >= 0 && total.is_some_and(|total| value < total) { - covered_selector_values.insert(value); - } else if expr_static_width(&expr, packed_dimensions) - .is_some_and(|label_width| label_width <= width) - { - covered_selector_values - .insert(coerce_const_parameter_value(value, width, false)); - } else { - labels_are_constant = false; - } - } else { - labels_are_constant = false; - } - } conditions.push(case_item_condition(case_expr.clone(), expr)); } if let Some(condition) = conditions.into_iter().reduce(|left, right| Expr::Binary { @@ -9550,12 +9547,6 @@ fn conditional_assignments_from_case_statement( } } - let complete_two_state_case = two_state_selector_width - .and_then(|width| 1usize.checked_shl(u32::try_from(width).ok()?)) - .is_some_and(|value_count| { - labels_are_constant && covered_selector_values.len() == value_count - }); - let mut prior_false = Vec::new(); let mut definitely_assigned_branches = Vec::new(); let branch_count = branches.len(); @@ -9652,6 +9643,65 @@ fn two_state_case_selector_width( .then(|| expr_static_width(selector, packed_dimensions))? } +fn two_state_case_items_cover_selector( + stmt: &sv_parser::CaseStatementNormal, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, +) -> bool { + if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { + return false; + } + let Some(selector) = expr_from_expression_with_types( + &stmt.nodes.2.nodes.1.nodes.0, + syntax_tree, + packed_dimensions, + ) else { + return false; + }; + let Some(width) = two_state_case_selector_width(&selector, packed_dimensions) else { + return false; + }; + let Some(value_count) = u32::try_from(width) + .ok() + .and_then(|width| 1usize.checked_shl(width)) + else { + return false; + }; + let Some(total) = i128::try_from(value_count).ok() else { + return false; + }; + let mut covered = HashSet::default(); + for item in std::iter::once(&stmt.nodes.3).chain(stmt.nodes.4.iter()) { + let sv_parser::CaseItem::NonDefault(item) = item else { + continue; + }; + for label in item.nodes.0.contents() { + let Some(label) = + expr_from_expression_with_types(&label.nodes.0, syntax_tree, packed_dimensions) + else { + return false; + }; + let Some(value) = expr_to_const(label.clone()) + .and_then(|label| eval_ast_const_expr(&label, const_env)) + else { + return false; + }; + let value = if value >= 0 && value < total { + value + } else if expr_static_width(&label, packed_dimensions) + .is_some_and(|label_width| label_width <= width) + { + coerce_const_parameter_value(value, width, false) + } else { + return false; + }; + covered.insert(value); + } + } + covered.len() == value_count +} + fn mark_condition_context( assignments: &mut [ConditionalAssignment], start: usize, @@ -9781,7 +9831,14 @@ fn definitely_assigned_comb_targets( ) }) .collect::>(); - if has_default { + if has_default + || two_state_case_items_cover_selector( + case, + syntax_tree, + &packed_dimensions.const_env, + packed_dimensions, + ) + { intersect_lvalue_sets(branches, packed_dimensions) } else { Vec::new() diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index ca3d2bfca..dc0b02c06 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2891,6 +2891,29 @@ fn preserves_four_state_masks_through_constant_casts() { 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#" @@ -2947,6 +2970,40 @@ fn recognizes_complete_cases_over_two_state_selectors() { 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 registers_enum_types_with_default_bases() { let source = r#" From ef9d2de263cb3583c64166c3986753a52f199ba8 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 22:36:50 +0900 Subject: [PATCH 15/55] fix(sv-frontend): infer two-state selector expressions --- crates/celox-sv-analyzer/src/ast.rs | 50 ++++++++++++++----- .../systemverilog/review_regressions.rs | 35 +++++++++++++ 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 8fcba7054..6e87905bb 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -9627,22 +9627,46 @@ fn two_state_case_selector_width( selector: &Expr, packed_dimensions: &PackedDimensions, ) -> Option { - let name = match selector { - Expr::Ident(name) => name, - Expr::Select { expr, .. } => { - let Expr::Ident(name) = &**expr else { - return None; - }; - name - } - _ => return None, - }; - packed_dimensions - .get(name) - .is_some_and(|dimensions| dimensions.is_2state) + expr_is_two_state(selector, packed_dimensions) .then(|| expr_static_width(selector, packed_dimensions))? } +fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool { + match expr { + Expr::Ident(name) => packed_dimensions + .get(name) + .is_some_and(|dimensions| dimensions.is_2state), + Expr::Literal(value) => typecheck::parse_integral_literal(value) + .is_some_and(|literal| literal.mask == num_bigint::BigUint::default()), + Expr::Select { expr, .. } | 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 | BinaryOp::EqWildcard | BinaryOp::NeWildcard + ) || (expr_is_two_state(left, packed_dimensions) + && expr_is_two_state(right, packed_dimensions)) + } + Expr::Mux { + condition, + then_expr, + else_expr, + } => { + expr_is_two_state(condition, packed_dimensions) + && expr_is_two_state(then_expr, packed_dimensions) + && expr_is_two_state(else_expr, packed_dimensions) + } + Expr::Call { .. } => false, + } +} + fn two_state_case_items_cover_selector( stmt: &sv_parser::CaseStatementNormal, syntax_tree: &SyntaxTree, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index dc0b02c06..1203f81a7 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2970,6 +2970,41 @@ fn recognizes_complete_cases_over_two_state_selectors() { assert_eq!(sim.get(y), 1u8.into()); } +#[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 recognizes_nested_complete_cases_as_definite_assignments() { let source = r#" From 757dfc6a5b3fb6d9fa1f156a7950e98276d74d4b Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 28 Aug 2026 23:18:36 +0900 Subject: [PATCH 16/55] fix(sv-frontend): preserve procedural dependency analysis --- crates/celox-sv-analyzer/src/ast.rs | 178 +++++++++++++++--- .../systemverilog/review_regressions.rs | 148 +++++++++++++++ 2 files changed, 297 insertions(+), 29 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 6e87905bb..7e2c136c5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -294,6 +294,8 @@ impl Module { syntax_tree, &const_env, &packed_dimensions, + &functions, + &expression_signedness, )? .into_iter() .map(|process| expand_process_calls(process, &functions, &expression_signedness)) @@ -5931,6 +5933,8 @@ fn comb_processes_from_module_node( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, ) -> Result, AnalyzerError> { let mut processes = Vec::new(); for item in module_non_port_items(node) { @@ -5940,6 +5944,8 @@ fn comb_processes_from_module_node( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, &mut processes, )?; } @@ -5952,6 +5958,8 @@ fn comb_processes_from_non_port_module_item( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { @@ -5963,6 +5971,8 @@ fn comb_processes_from_non_port_module_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -5974,6 +5984,8 @@ fn comb_processes_from_non_port_module_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -5988,6 +6000,8 @@ fn comb_processes_from_generate_item( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { @@ -5997,6 +6011,8 @@ fn comb_processes_from_generate_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6009,6 +6025,8 @@ fn comb_processes_from_module_or_generate_item( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { @@ -6018,6 +6036,8 @@ fn comb_processes_from_module_or_generate_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6030,6 +6050,8 @@ fn comb_processes_from_module_common_item( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { @@ -6055,6 +6077,8 @@ fn comb_processes_from_module_common_item( condition, syntax_tree, packed_dimensions, + functions, + expression_signedness, )? { processes.push(substitute_process_constants(process, const_env)); } @@ -6066,6 +6090,8 @@ fn comb_processes_from_module_common_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6076,6 +6102,8 @@ fn comb_processes_from_module_common_item( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6095,6 +6123,8 @@ fn comb_processes_from_conditional_generate( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { @@ -6121,6 +6151,8 @@ fn comb_processes_from_conditional_generate( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6141,6 +6173,8 @@ fn comb_processes_from_conditional_generate( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; if let Some((_, block)) = &generate.nodes.3 { @@ -6157,6 +6191,8 @@ fn comb_processes_from_conditional_generate( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6169,6 +6205,8 @@ fn comb_processes_from_loop_generate( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if generate_block_has_data_declaration(&generate.nodes.2) { @@ -6208,6 +6246,8 @@ fn comb_processes_from_loop_generate( syntax_tree, &loop_env, packed_dimensions, + functions, + expression_signedness, processes, )?; iterations += 1; @@ -6507,6 +6547,8 @@ fn comb_processes_from_generate_block( syntax_tree: &SyntaxTree, const_env: &HashMap, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match block { @@ -6517,6 +6559,8 @@ fn comb_processes_from_generate_block( syntax_tree, const_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -6532,6 +6576,8 @@ fn comb_processes_from_generate_block( syntax_tree, &block_env, packed_dimensions, + functions, + expression_signedness, processes, )?; } @@ -7403,6 +7449,8 @@ fn comb_process_from_always_construct( condition: Option, syntax_tree: &SyntaxTree, packed_dimensions: &PackedDimensions, + functions: &HashMap, + expression_signedness: &HashMap, ) -> Result, AnalyzerError> { if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) { return Ok(None); @@ -7418,6 +7466,17 @@ fn comb_process_from_always_construct( packed_dimensions, &mut guarded_assignments, )?; + for guarded in &mut guarded_assignments { + guarded.condition = guarded.condition.take().map(|condition| { + expand_expr_calls(condition, functions, expression_signedness, 0, true) + }); + guarded.assignment = expand_assignment_calls( + guarded.assignment.clone(), + functions, + expression_signedness, + true, + ); + } let assignments = comb_assignments_from_guarded(guarded_assignments, packed_dimensions)?; Ok((!assignments.is_empty()) .then(|| CombProcess::new(CombProcessKind::AlwaysComb, condition, assignments))) @@ -8408,8 +8467,8 @@ fn substitute_expr_lvalue( value, packed_dimensions, )), - msb, - lsb, + 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( @@ -8497,6 +8556,22 @@ fn substitute_expr_lvalue( } } +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, @@ -8639,7 +8714,12 @@ fn expr_references_lvalue(expr: &Expr, target: &LValue) -> bool { } match expr { Expr::Ident(_) | Expr::Literal(_) => false, - Expr::Select { expr, .. } | Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { + Expr::Select { expr, msb, lsb, .. } => { + expr_references_lvalue(expr, target) + || expr_references_lvalue(&const_expr_to_expr(msb.clone()), target) + || expr_references_lvalue(&const_expr_to_expr(lsb.clone()), target) + } + Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { expr_references_lvalue(expr, target) } Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts @@ -8683,7 +8763,7 @@ fn expr_references_overlapping_lvalue( lsb, .. } => { - if let Expr::Ident(read_name) = &**selected { + let selected_reference = if let Expr::Ident(read_name) = &**selected { match target { LValue::Ident(target_name) => read_name == target_name, LValue::Select { @@ -8707,7 +8787,18 @@ fn expr_references_overlapping_lvalue( } } 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) @@ -9692,10 +9783,18 @@ fn two_state_case_items_cover_selector( else { return false; }; - let Some(total) = i128::try_from(value_count).ok() else { + if i128::try_from(value_count).is_err() { + return false; + } + let identifiers = packed_dimensions + .iter() + .map(|(name, dimensions)| (name.clone(), dimensions.signed)) + .collect::>(); + let Some(selector_signed) = expr_signedness(&selector, &identifiers, &HashMap::default()) + else { return false; }; - let mut covered = HashSet::default(); + let mut labels = Vec::new(); for item in std::iter::once(&stmt.nodes.3).chain(stmt.nodes.4.iter()) { let sv_parser::CaseItem::NonDefault(item) = item else { continue; @@ -9706,24 +9805,31 @@ fn two_state_case_items_cover_selector( else { return false; }; - let Some(value) = expr_to_const(label.clone()) - .and_then(|label| eval_ast_const_expr(&label, const_env)) - else { - return false; - }; - let value = if value >= 0 && value < total { - value - } else if expr_static_width(&label, packed_dimensions) - .is_some_and(|label_width| label_width <= width) - { - coerce_const_parameter_value(value, width, false) - } else { - return false; - }; - covered.insert(value); + if let Some(label) = expr_to_const(label) { + labels.push(label); + } } } - covered.len() == value_count + if labels.len() < value_count { + return false; + } + (0..value_count).all(|value| { + let selector = ConstExpr::Literal(format_typed_parameter_literal( + value as i128, + width, + selector_signed, + )); + labels.iter().any(|label| { + eval_ast_const_expr( + &ConstExpr::Binary { + left: Box::new(selector.clone()), + op: BinaryOp::EqCase, + right: Box::new(label.clone()), + }, + const_env, + ) == Some(1) + }) + }) } fn mark_condition_context( @@ -11569,12 +11675,26 @@ 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 + } } } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 1203f81a7..968def03a 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -55,6 +55,106 @@ 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 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 rejects_inline_enum_ports_instead_of_scalarizing_them() { let error = cranelift_build_error( @@ -3005,6 +3105,54 @@ fn recognizes_complete_cases_over_two_state_expressions() { 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#" From 62fb61d52e585ad779896491ba36ad8319bf9f94 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 00:30:51 +0900 Subject: [PATCH 17/55] fix(sv-frontend): preserve guarded type semantics --- crates/celox-sv-analyzer/src/ast.rs | 96 +++++++++++++--- .../systemverilog/review_regressions.rs | 106 +++++++++++++++++- 2 files changed, 187 insertions(+), 15 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 7e2c136c5..26e38b1bb 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -296,6 +296,7 @@ impl Module { &packed_dimensions, &functions, &expression_signedness, + ¶meter_values, )? .into_iter() .map(|process| expand_process_calls(process, &functions, &expression_signedness)) @@ -1134,9 +1135,15 @@ fn reject_silently_ignored_constructs( .into_iter() .any(|node| matches!(node, RefNode::DataDeclaration(_))) { - return Err(AnalyzerError::Unsupported( - "procedural local data declaration".to_string(), - )); + 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() => { @@ -3391,6 +3398,11 @@ fn enum_member_constants_from_module_node( .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" @@ -3471,10 +3483,28 @@ fn parameter_value_env( &values, ); if let Some(width) = width { - Expr::Resize { - expr: Box::new(value), - width, - signed, + 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 @@ -5935,6 +5965,7 @@ fn comb_processes_from_module_node( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, ) -> Result, AnalyzerError> { let mut processes = Vec::new(); for item in module_non_port_items(node) { @@ -5946,6 +5977,7 @@ fn comb_processes_from_module_node( packed_dimensions, functions, expression_signedness, + parameter_literals, &mut processes, )?; } @@ -5960,6 +5992,7 @@ fn comb_processes_from_non_port_module_item( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { @@ -5973,6 +6006,7 @@ fn comb_processes_from_non_port_module_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -5986,6 +6020,7 @@ fn comb_processes_from_non_port_module_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6002,6 +6037,7 @@ fn comb_processes_from_generate_item( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item { @@ -6013,6 +6049,7 @@ fn comb_processes_from_generate_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6027,6 +6064,7 @@ fn comb_processes_from_module_or_generate_item( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if let sv_parser::ModuleOrGenerateItem::ModuleItem(item) = item { @@ -6038,6 +6076,7 @@ fn comb_processes_from_module_or_generate_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6052,6 +6091,7 @@ fn comb_processes_from_module_common_item( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match item { @@ -6079,6 +6119,7 @@ fn comb_processes_from_module_common_item( packed_dimensions, functions, expression_signedness, + parameter_literals, )? { processes.push(substitute_process_constants(process, const_env)); } @@ -6092,6 +6133,7 @@ fn comb_processes_from_module_common_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6104,6 +6146,7 @@ fn comb_processes_from_module_common_item( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6125,6 +6168,7 @@ fn comb_processes_from_conditional_generate( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { let sv_parser::ConditionalGenerateConstruct::If(generate) = generate else { @@ -6153,6 +6197,7 @@ fn comb_processes_from_conditional_generate( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6175,6 +6220,7 @@ fn comb_processes_from_conditional_generate( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; if let Some((_, block)) = &generate.nodes.3 { @@ -6193,6 +6239,7 @@ fn comb_processes_from_conditional_generate( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6207,6 +6254,7 @@ fn comb_processes_from_loop_generate( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { if generate_block_has_data_declaration(&generate.nodes.2) { @@ -6248,6 +6296,7 @@ fn comb_processes_from_loop_generate( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; iterations += 1; @@ -6549,6 +6598,7 @@ fn comb_processes_from_generate_block( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, processes: &mut Vec, ) -> Result<(), AnalyzerError> { match block { @@ -6561,6 +6611,7 @@ fn comb_processes_from_generate_block( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -6578,6 +6629,7 @@ fn comb_processes_from_generate_block( packed_dimensions, functions, expression_signedness, + parameter_literals, processes, )?; } @@ -7451,6 +7503,7 @@ fn comb_process_from_always_construct( packed_dimensions: &PackedDimensions, functions: &HashMap, expression_signedness: &HashMap, + parameter_literals: &HashMap, ) -> Result, AnalyzerError> { if !matches!(always.nodes.0, sv_parser::AlwaysKeyword::AlwaysComb(_)) { return Ok(None); @@ -7468,13 +7521,21 @@ fn comb_process_from_always_construct( )?; for guarded in &mut guarded_assignments { guarded.condition = guarded.condition.take().map(|condition| { - expand_expr_calls(condition, functions, expression_signedness, 0, true) + substitute_expr_constants_with_parameter_literals( + expand_expr_calls(condition, functions, expression_signedness, 0, true), + &HashMap::default(), + parameter_literals, + ) }); - guarded.assignment = expand_assignment_calls( - guarded.assignment.clone(), - functions, - expression_signedness, - true, + 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)?; @@ -8862,6 +8923,11 @@ fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), Ana Ok(()) } 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)?; @@ -9443,9 +9509,11 @@ fn lvalue_expr_type(value: &LValue, packed_dimensions: &PackedDimensions) -> Opt 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.width, &packed_dimensions.const_env)?; + eval_ast_const_expr(dimension, &packed_dimensions.const_env)?; width.checked_mul(usize::try_from(dimension_width).ok()?) })?; Some(ExprType { diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 968def03a..f8a621feb 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -78,6 +78,30 @@ fn rejects_self_reads_hidden_in_comb_function_calls() { ); } +#[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#" @@ -3206,6 +3230,86 @@ fn registers_enum_types_with_default_bases() { assert_eq!(sim.get(sim.signal("y")), 1u8.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 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" @@ -4405,7 +4509,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 From a13bc75376afa6273dd1b42aa411162a2b51c6fd Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 00:57:49 +0900 Subject: [PATCH 18/55] fix(sv-frontend): preserve elaboration constant types --- crates/celox-sv-analyzer/src/ast.rs | 291 ++++++++++++++++++++-------- crates/celox-sv-analyzer/src/lib.rs | 68 +++++++ 2 files changed, 281 insertions(+), 78 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 26e38b1bb..748a0c968 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -206,7 +206,7 @@ impl Module { } extend_const_env_with_parameters(&mut const_env, ¶meters); reject_silently_ignored_constructs(node.clone(), syntax_tree, &const_env, &type_aliases)?; - let ports = ports_from_module_node(node.clone(), syntax_tree)?; + 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 { @@ -1377,14 +1377,18 @@ fn reject_silently_ignored_constructs( )); } RefNode::PackedDimensionRange(range) - if const_expr_from_ref_node( + 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( + || const_expr_from_ref_node_with_env( RefNode::ConstantExpression(&range.nodes.0.nodes.1.nodes.2), syntax_tree, + const_env, + type_aliases, ) .is_none() => { @@ -2465,9 +2469,10 @@ fn identifier_locate(node: RefNode<'_>) -> Option { fn ports_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, ) -> 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 { @@ -2478,17 +2483,20 @@ fn ports_from_module_node( .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()) - })?, + 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( + let r#type = type_with_fallback_ranges_with_env( r#type, RefNode::AnsiPortDeclarationNet(port), syntax_tree, - &type_aliases, + const_env, + type_aliases, ); let inherited_type_base = r#type.clone(); let r#type = type_with_unpacked_ranges( @@ -2507,18 +2515,19 @@ fn ports_from_module_node( .unwrap_or(inherited_direction); let r#type = match header { Some(header) => { - type_from_variable_port_header(header, syntax_tree, &type_aliases) + 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( + let r#type = type_with_fallback_ranges_with_env( r#type, RefNode::AnsiPortDeclarationVariable(port), syntax_tree, - &type_aliases, + const_env, + type_aliases, ); let inherited_type_base = r#type.clone(); let r#type = type_with_unpacked_ranges( @@ -2738,10 +2747,10 @@ fn signals_from_module_common_item( }; let mut declared = match &**declaration { sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) => { - signals_from_data_declaration(data, syntax_tree, type_aliases)? + 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)? + signals_from_net_declaration(net, syntax_tree, type_aliases, const_env)? } _ => Vec::new(), }; @@ -2821,17 +2830,23 @@ 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(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 = 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(), @@ -2841,10 +2856,11 @@ fn signals_from_net_declaration( )); } }; - let r#type = type_with_fallback_ranges( + let r#type = type_with_fallback_ranges_with_env( r#type, RefNode::DataTypeOrImplicit(&net.nodes.3), syntax_tree, + const_env, type_aliases, ); (r#type, net.nodes.5.nodes.0.contents(), true) @@ -3098,18 +3114,24 @@ 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(RefNode::DataTypeOrImplicit(&variable.nodes.3), syntax_tree) - .or_else(|| { - type_alias_from_ref_node( - RefNode::DataTypeOrImplicit(&variable.nodes.3), - syntax_tree, - type_aliases, - ) - }); + 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(), @@ -3119,10 +3141,11 @@ fn signals_from_data_declaration( )); } }; - let r#type = type_with_fallback_ranges( + 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(); @@ -3369,8 +3392,37 @@ fn enum_member_constants_from_module_node( let Some(declaration) = package_or_generate_declaration_from_non_port_item(item) else { continue; }; - let sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) = declaration 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, + type_aliases, + &HashMap::default(), + )?; + extend_const_env_with_parameters(&mut eval_env, ¶meters); + 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, + type_aliases, + &HashMap::default(), + )?; + extend_const_env_with_parameters(&mut eval_env, ¶meters); + continue; + } + sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(data) => data, + _ => continue, }; let sv_parser::DataDeclaration::TypeDeclaration(type_declaration) = &**data else { continue; @@ -4428,7 +4480,7 @@ fn instances_from_module_instantiation( override_.value = override_ .value .take() - .map(|value| substitute_const_expr_constants(value, const_env)); + .map(|value| substitute_const_expr_constants_preserving_enum_types(value, const_env)); } let condition = condition.map(|condition| substitute_const_expr_constants(condition, const_env)); @@ -5040,6 +5092,7 @@ fn function_from_declaration( 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::>(); @@ -5098,6 +5151,7 @@ fn function_from_declaration( function_local_packed_dimensions_from_block_item_iter( block_items.iter().copied(), syntax_tree, + const_env, type_aliases, )?, ); @@ -5156,14 +5210,21 @@ fn function_local_types_from_block_items( fn function_local_packed_dimensions_from_block_items( items: &[sv_parser::BlockItemDeclaration], syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Option { - function_local_packed_dimensions_from_block_item_iter(items.iter(), syntax_tree, type_aliases) + function_local_packed_dimensions_from_block_item_iter( + items.iter(), + syntax_tree, + const_env, + type_aliases, + ) } fn function_local_packed_dimensions_from_block_item_iter<'a>( items: impl IntoIterator, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Option { let mut dimensions = HashMap::default(); @@ -5172,7 +5233,8 @@ fn function_local_packed_dimensions_from_block_item_iter<'a>( continue; }; let signals = - signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases).ok()?; + 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(), @@ -5200,7 +5262,8 @@ fn function_local_types_from_block_item_iter<'a>( continue; }; let signals = - signals_from_data_declaration(&item.nodes.1, syntax_tree, type_aliases).ok()?; + 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() { @@ -6445,7 +6508,7 @@ fn generate_block_direct_data_declaration_names( return; }; names.extend( - signals_from_data_declaration(data, syntax_tree, &aliases) + signals_from_data_declaration(data, syntax_tree, &aliases, &HashMap::default()) .unwrap_or_default() .into_iter() .map(|signal| signal.name), @@ -7351,39 +7414,63 @@ fn substitute_const_expr_constants( expr: ConstExpr, const_env: &HashMap, ) -> ConstExpr { - substitute_const_expr_constants_impl(expr, const_env, false) + substitute_const_expr_constants_impl(expr, const_env, false, false) } fn substitute_dimension_constants(expr: ConstExpr, const_env: &HashMap) -> ConstExpr { - substitute_const_expr_constants_impl(expr, const_env, true) + substitute_const_expr_constants_impl(expr, const_env, true, false) +} + +fn substitute_const_expr_constants_preserving_enum_types( + expr: ConstExpr, + const_env: &HashMap, +) -> ConstExpr { + substitute_const_expr_constants_impl(expr, const_env, false, true) } 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) => const_env - .get(&name) - .filter(|_| { + 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)) - }) - .map(|value| ConstExpr::Literal(value.to_string())) - .unwrap_or(ConstExpr::Ident(name)), + }) else { + return ConstExpr::Ident(name); + }; + 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 { @@ -7391,7 +7478,12 @@ fn substitute_const_expr_constants_impl( args: args .into_iter() .map(|arg| { - substitute_const_expr_constants_impl(arg, const_env, include_local_parameters) + substitute_const_expr_constants_impl( + arg, + const_env, + include_local_parameters, + preserve_enum_types, + ) }) .collect(), }, @@ -7401,6 +7493,7 @@ fn substitute_const_expr_constants_impl( *expr, const_env, include_local_parameters, + preserve_enum_types, )), }, ConstExpr::Binary { left, op, right } => ConstExpr::Binary { @@ -7408,12 +7501,14 @@ fn 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 { @@ -7425,16 +7520,19 @@ fn 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, )), }, } @@ -11821,6 +11919,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 @@ -11846,7 +11953,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 { @@ -11873,16 +11982,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)?; @@ -11895,25 +12007,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) + }), }, } } @@ -11946,13 +12061,15 @@ 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); } else if r#type.packed_ranges.is_empty() { @@ -11982,17 +12099,35 @@ fn is_signed_from_ref_node(node: RefNode<'_>) -> Option { } fn packed_ranges_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Vec { + packed_ranges_from_ref_node_with_env( + node, + syntax_tree, + &HashMap::default(), + &HashMap::default(), + ) +} + +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)); diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index ac6692f1a..46cb449e0 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -903,6 +903,74 @@ mod tests { 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 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 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( From 47cd494b0d493128b51e51be73809158ec815ca0 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 01:30:59 +0900 Subject: [PATCH 19/55] fix(sv-frontend): preserve unpacked elaboration semantics --- crates/celox-sv-analyzer/src/ast.rs | 172 ++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 39 ++++ .../systemverilog/review_regressions.rs | 40 ++++ 3 files changed, 220 insertions(+), 31 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 748a0c968..f6a7b9569 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -2501,7 +2501,12 @@ fn ports_from_module_node( 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)?, + 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; @@ -2532,7 +2537,12 @@ fn ports_from_module_node( 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)?, + unpacked_ranges_from_variable_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; @@ -2711,8 +2721,12 @@ fn signals_from_module_or_generate_item( ) -> 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)?; + 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); } @@ -2889,7 +2903,12 @@ fn signals_from_net_declaration( })?; let signal_type = type_with_unpacked_ranges( r#type.clone(), - unpacked_ranges_from_dimensions(&assignment.nodes.1, syntax_tree)?, + 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) @@ -2904,6 +2923,7 @@ fn signals_from_type_alias_instantiation( instantiation: &sv_parser::ModuleInstantiation, syntax_tree: &SyntaxTree, type_aliases: &HashMap, + const_env: &HashMap, ) -> Result, AnalyzerError> { let mut signals = Vec::new(); let module_name = identifier_text( @@ -2924,7 +2944,12 @@ fn signals_from_type_alias_instantiation( .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)?, + unpacked_ranges_from_dimensions_with_env( + &instance.nodes.0.nodes.1, + syntax_tree, + const_env, + type_aliases, + )?, ); signals.push(Signal::new(name, signal_type)); } @@ -3160,7 +3185,12 @@ fn signals_from_data_declaration( .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)?, + unpacked_ranges_from_variable_dimensions_with_env( + &assignment.nodes.1, + syntax_tree, + const_env, + type_aliases, + )?, ); signals.push(Signal::new(name, signal_type)); } @@ -3172,6 +3202,18 @@ fn type_alias_from_ref_node( syntax_tree: &SyntaxTree, type_aliases: &HashMap, ) -> Option { + 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); + } + 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)? @@ -3210,8 +3252,14 @@ fn parameters_from_ref_node( "unsupported parameter data type".to_string(), )); } - let parameter_width = - parameter_declared_width(node.clone(), syntax_tree, base_const_env, parameters); + let declared_alias = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases); + let parameter_width = parameter_declared_width( + node.clone(), + syntax_tree, + base_const_env, + parameters, + type_aliases, + ); let has_declared_type = node.clone().into_iter().any(|child| { matches!( child, @@ -3219,11 +3267,17 @@ fn parameters_from_ref_node( ) }); 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)) + declared_alias + .as_ref() + .map(Type::is_signed) + .unwrap_or_else(|| { + 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) + let parameter_is_2state = declared_alias + .or_else(|| 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 { @@ -3261,9 +3315,17 @@ fn parameter_declared_width( syntax_tree: &SyntaxTree, base_const_env: &HashMap, parameters: &[Parameter], + type_aliases: &HashMap, ) -> Option { - let ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); + let declared_alias = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases); + let ranges = declared_alias + .as_ref() + .map(|r#type| r#type.packed_ranges.clone()) + .unwrap_or_else(|| packed_ranges_from_ref_node(node.clone(), syntax_tree)); 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); } @@ -8096,23 +8158,32 @@ fn simplify_single_bit_concat_selects(expr: Expr, packed_dimensions: &PackedDime signed, } => { let expr = simplify_single_bit_concat_selects(*expr, packed_dimensions); - let bit = match ( + 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)) if msb == lsb => Some(msb), + (Some(msb), Some(lsb)) => Some((msb, lsb)), _ => None, }; - if let (Some(bit), Expr::Concat(parts)) = (bit, &expr) - && let Ok(bit) = usize::try_from(bit) + 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; }; - if bit < offset.saturating_add(width) { - let selected = bit - offset; + 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 { @@ -8124,7 +8195,7 @@ fn simplify_single_bit_concat_selects(expr: Expr, packed_dimensions: &PackedDime } }; } - offset = offset.saturating_add(width); + offset = end; } } Expr::Select { @@ -8387,10 +8458,10 @@ fn overlapping_value_before( fn whole_packed_lvalue(name: &str, packed_dimensions: &PackedDimensions) -> Option { let dimensions = packed_dimensions.get(name)?; - if !dimensions.unpacked.is_empty() { - return None; - } - let (msb, lsb) = if dimensions.packed.len() == 1 && !dimensions.packed[0].normalize_single { + 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(), @@ -8401,6 +8472,12 @@ fn whole_packed_lvalue(name: &str, packed_dimensions: &PackedDimensions) -> Opti .packed .iter() .map(|dimension| dimension.width.clone()) + .chain( + dimensions + .unpacked + .iter() + .map(|dimension| dimension.width.clone()), + ) .collect::>(), ); ( @@ -12137,22 +12214,28 @@ fn packed_ranges_from_ref_node_with_env( 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)), @@ -12162,9 +12245,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()) @@ -12194,14 +12279,30 @@ fn validate_unpacked_dimension_sizes( fn unpacked_ranges_from_variable_dimensions( dimensions: &[sv_parser::VariableDimension], syntax_tree: &SyntaxTree, +) -> Result, AnalyzerError> { + unpacked_ranges_from_variable_dimensions_with_env( + dimensions, + syntax_tree, + &HashMap::default(), + &HashMap::default(), + ) +} + +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(_) => { @@ -12309,7 +12410,16 @@ fn const_expr_from_ref_node_with_env( const_select_expr(base.clone(), ¶meter.nodes.1, syntax_tree).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; }; diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 46cb449e0..f13b9c047 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -945,6 +945,45 @@ mod tests { ); } + #[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 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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index f8a621feb..7609791c9 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3310,6 +3310,46 @@ fn coerces_whole_unpacked_array_writes_to_the_flattened_width() { 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 interprets_signed_literals_before_constant_unary_operations() { let source = r#" From ef921b76dae6e5d8660cf21e363d69e064f3bc19 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 01:56:43 +0900 Subject: [PATCH 20/55] fix(sv-frontend): preserve exhaustive branch semantics --- crates/celox-sv-analyzer/src/ast.rs | 114 +++++++++++++----- crates/celox-sv-analyzer/src/lib.rs | 19 +++ .../systemverilog/review_regressions.rs | 94 +++++++++++++++ 3 files changed, 199 insertions(+), 28 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index f6a7b9569..cb49aa303 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -831,11 +831,12 @@ fn cast_zero_type( 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 - }; + 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 @@ -864,13 +865,14 @@ fn constant_cast_const_expr( typecheck::parse_integral_literal(&operand_literal)? }; let target_type = cast_target_type(&cast.nodes.0, syntax_tree, const_env, type_aliases)?; - // A size cast keeps the source expression's signedness when the target - // is described by a constant primary; a type cast takes the target's. - let signed = if matches!(cast.nodes.0, sv_parser::CastingType::ConstantPrimary(_)) { - literal.signed - } else { - target_type.signed - }; + // 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( @@ -882,6 +884,29 @@ fn constant_cast_const_expr( Some(ConstExpr::Literal(resized)) } +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(_) => true, + 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(); @@ -7839,7 +7864,8 @@ fn normalize_mixed_whole_selected_comb_writes( packed_dimensions: &PackedDimensions, ) { let mut whole_names = HashSet::default(); - let mut selected_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() { @@ -7849,13 +7875,30 @@ fn normalize_mixed_whole_selected_comb_writes( whole_names.insert(name.clone()); } LValue::Ident(_) => {} - LValue::Select { name, .. } => { - selected_names.insert(name.clone()); + 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()); } } } - whole_names.retain(|name| selected_names.contains(name)); + whole_names.retain(|name| selected_targets.contains_key(name)); + 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)) + { + whole_names.insert(name); + } + } for write in guarded.iter_mut() { let target = write.assignment().lhs_value().clone(); let LValue::Select { name, .. } = &target else { @@ -9982,11 +10025,9 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool *op == UnaryOp::ToTwoState || expr_is_two_state(expr, packed_dimensions) } Expr::Binary { left, op, right } => { - matches!( - op, - BinaryOp::EqCase | BinaryOp::NeCase | BinaryOp::EqWildcard | BinaryOp::NeWildcard - ) || (expr_is_two_state(left, packed_dimensions) - && expr_is_two_state(right, packed_dimensions)) + matches!(op, BinaryOp::EqCase | BinaryOp::NeCase) + || (expr_is_two_state(left, packed_dimensions) + && expr_is_two_state(right, packed_dimensions)) } Expr::Mux { condition, @@ -10099,16 +10140,33 @@ fn mark_exhaustive_fallback( chain_start: usize, packed_dimensions: &PackedDimensions, ) { - let mut marked_targets = Vec::new(); - for assignment in fallback_assignments { - let target = assignment.assignment().lhs_value(); + 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)) - && !marked_targets.contains(target) + .all(|targets| lvalue_is_covered_by(&target, targets, packed_dimensions)) + && !definite_targets.contains(&target) { - marked_targets.push(target.clone()); - assignment.exhaustive_fallback_start = Some(chain_start); + 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); } } } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index f13b9c047..ef28e4dc5 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -965,6 +965,25 @@ mod tests { ); } + #[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 resolves_typedef_declared_parameter_types() { let ir = analyze_source( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 7609791c9..6f5d404e8 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3350,6 +3350,100 @@ fn normalizes_element_writes_before_conditional_whole_array_writes() { 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 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 + case (a ==? 1'b0) + 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 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 = 1'b0; + if (c) + x = a; + else begin + if (d) + x = b; + y = x; + x = e; + end + end + endmodule + "#, + ); + assert!( + error.contains("inside always_comb"), + "unexpected error: {error}" + ); +} + #[test] fn interprets_signed_literals_before_constant_unary_operations() { let source = r#" From 273c55d581756580c40d745b3cc176f3fa840a27 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 02:32:06 +0900 Subject: [PATCH 21/55] fix(sv-frontend): preserve nested constant casts --- crates/celox-sv-analyzer/src/ast.rs | 152 +++++++++++++++++++++------- crates/celox-sv-analyzer/src/lib.rs | 49 +++++++++ 2 files changed, 164 insertions(+), 37 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index cb49aa303..1d0a403f9 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -149,7 +149,7 @@ impl Module { ) -> Result { let node = node.into(); let name = module_name_from_node(node.clone(), syntax_tree)?; - let type_aliases = type_aliases_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 @@ -205,6 +205,8 @@ impl Module { 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)?; let ports = ports_from_module_node(node.clone(), syntax_tree, &const_env, &type_aliases)?; let mut port_names = HashSet::default(); @@ -220,7 +222,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) @@ -881,9 +884,61 @@ fn constant_cast_const_expr( } _ => 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, @@ -2672,14 +2727,14 @@ fn signals_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, const_env: &HashMap, + type_aliases: &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, syntax_tree, - &type_aliases, + type_aliases, const_env, &mut signals, )?; @@ -2984,6 +3039,14 @@ fn signals_from_type_alias_instantiation( 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, ) -> Result, AnalyzerError> { let mut aliases = HashMap::default(); if let Some(parameter_port_list) = module_parameter_port_list(node.clone()) { @@ -3012,7 +3075,12 @@ fn type_aliases_from_module_node( }; match declaration { sv_parser::PackageOrGenerateItemDeclaration::DataDeclaration(declaration) => { - add_type_alias_from_data_declaration(declaration, syntax_tree, &mut aliases)?; + 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, &mut aliases); @@ -3035,6 +3103,7 @@ fn type_aliases_from_module_node( fn add_type_alias_from_data_declaration( declaration: &sv_parser::DataDeclaration, syntax_tree: &SyntaxTree, + const_env: &HashMap, aliases: &mut HashMap, ) -> Result<(), AnalyzerError> { let sv_parser::DataDeclaration::TypeDeclaration(declaration) = declaration else { @@ -3047,29 +3116,39 @@ fn add_type_alias_from_data_declaration( else { return Ok(()); }; - let r#type = - type_from_ref_node(RefNode::DataType(&declaration.nodes.1), syntax_tree).or_else(|| { - let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 else { - return None; - }; - if let Some(base) = &r#enum.nodes.1 { - 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) - } - }); + let r#type = type_from_ref_node_with_env( + RefNode::DataType(&declaration.nodes.1), + syntax_tree, + const_env, + aliases, + ) + .or_else(|| { + let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 else { + return None; + }; + if let Some(base) = &r#enum.nodes.1 { + 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) + } + }); let Some(r#type) = r#type else { return Ok(()); }; let r#type = type_with_unpacked_ranges( r#type, - unpacked_ranges_from_variable_dimensions(&declaration.nodes.3, syntax_tree)?, + unpacked_ranges_from_variable_dimensions_with_env( + &declaration.nodes.3, + syntax_tree, + const_env, + aliases, + )?, ); aliases.insert(name, r#type); Ok(()) @@ -12334,18 +12413,6 @@ fn validate_unpacked_dimension_sizes( Ok(()) } -fn unpacked_ranges_from_variable_dimensions( - dimensions: &[sv_parser::VariableDimension], - syntax_tree: &SyntaxTree, -) -> Result, AnalyzerError> { - unpacked_ranges_from_variable_dimensions_with_env( - dimensions, - syntax_tree, - &HashMap::default(), - &HashMap::default(), - ) -} - fn unpacked_ranges_from_variable_dimensions_with_env( dimensions: &[sv_parser::VariableDimension], syntax_tree: &SyntaxTree, @@ -12465,7 +12532,14 @@ fn const_expr_from_ref_node_with_env( 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) => { let lowered = const_expr_from_function_subroutine_call(&call.nodes.0, syntax_tree); @@ -12568,14 +12642,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 ef28e4dc5..0496a3755 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -984,6 +984,55 @@ mod tests { assert_eq!(parameters[2].resolved_value(), Some(-1)); } + #[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( From 93af484f11f7ee6e505c054508718fd3297d5623 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 04:38:05 +0900 Subject: [PATCH 22/55] fix(sv-frontend): preserve dynamic selected fallbacks --- crates/celox-sv-analyzer/src/ast.rs | 104 ++++++++++++++++- .../systemverilog/review_regressions.rs | 108 ++++++++++++++++++ 2 files changed, 210 insertions(+), 2 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 1d0a403f9..f20a8fa51 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8668,8 +8668,21 @@ fn selected_value_after_write( } 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 = eval_ast_const_expr(write_msb_expr, const_env)?; - let write_lsb = eval_ast_const_expr(write_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, + ); + } + }; 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)); @@ -8728,6 +8741,93 @@ fn selected_value_after_write( } } +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; + }; + + // 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 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 { + continue; + }; + 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 whole_write_select_offsets( name: &str, msb: &ConstExpr, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 6f5d404e8..556d589e1 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -1504,6 +1504,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#" From 3171bc9362bb2248ea5e69b1d52ef7c616eba534 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 05:12:44 +0900 Subject: [PATCH 23/55] fix(sv-frontend): preserve conditional type semantics --- crates/celox-sv-analyzer/src/ast.rs | 95 +++++++++++++++---- .../systemverilog/review_regressions.rs | 77 +++++++++++++++ 2 files changed, 154 insertions(+), 18 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index f20a8fa51..b759a9cb4 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -177,6 +177,8 @@ impl Module { apply_parameter_overrides(&mut parameters, parameter_overrides)?; } 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, @@ -10194,9 +10196,11 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool .is_some_and(|dimensions| dimensions.is_2state), Expr::Literal(value) => typecheck::parse_integral_literal(value) .is_some_and(|literal| literal.mask == num_bigint::BigUint::default()), - Expr::Select { expr, .. } | Expr::Resize { expr, .. } => { + 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)), @@ -10221,6 +10225,42 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool } } +fn select_bounds_are_statically_valid( + expr: &Expr, + msb: &ConstExpr, + lsb: &ConstExpr, + packed_dimensions: &PackedDimensions, +) -> 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 two_state_case_items_cover_selector( stmt: &sv_parser::CaseStatementNormal, syntax_tree: &SyntaxTree, @@ -10397,26 +10437,45 @@ fn definitely_assigned_comb_targets( targets } sv_parser::StatementItem::ConditionalStatement(conditional) => { - let Some((_, else_branch)) = &conditional.nodes.5 else { - return Vec::new(); - }; - let mut branches = vec![definitely_assigned_comb_targets_statement_or_null( - &conditional.nodes.3, - syntax_tree, - packed_dimensions, - )]; - branches.extend(conditional.nodes.4.iter().map(|(_, _, _, branch)| { - definitely_assigned_comb_targets_statement_or_null( + 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) + .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, - ) - })); - branches.push(definitely_assigned_comb_targets_statement_or_null( - else_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::CaseStatement(case) => { diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 556d589e1..d965b6df9 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3054,6 +3054,45 @@ fn accepts_constant_true_always_comb_guards() { 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 registers_enum_types_with_aliased_bases() { let source = r#" @@ -3074,6 +3113,24 @@ fn registers_enum_types_with_aliased_bases() { 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 coerces_selected_writes_before_whole_vector_normalization() { let source = r#" @@ -3202,6 +3259,26 @@ fn recognizes_complete_cases_over_two_state_selectors() { assert_eq!(sim.get(y), 1u8.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 recognizes_complete_cases_over_two_state_expressions() { let source = r#" From 1df3f587bc046f1fea939e3b104213e5d5ce2c60 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 15:43:57 +0900 Subject: [PATCH 24/55] fix(sv-frontend): recognize complementary comb guards --- crates/celox-sv-analyzer/src/ast.rs | 100 +++++++++++++++++- .../systemverilog/review_regressions.rs | 81 ++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index b759a9cb4..373a544c5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -10423,16 +10423,54 @@ fn definitely_assigned_comb_targets( } 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 { - for target in definitely_assigned_comb_targets_statement_or_null( + let statement_targets = definitely_assigned_comb_targets_statement_or_null( stmt, syntax_tree, packed_dimensions, - ) { + ); + guarded_targets.retain(|(condition, _)| { + !statement_targets + .iter() + .any(|target| expr_references_lvalue(condition, target)) + }); + 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 { + if !statement_or_null_is_blocking_assignment(stmt) { + guarded_targets.clear(); + } + continue; + }; + guarded_targets.retain(|(prior_condition, _)| { + !branch_targets + .iter() + .any(|target| expr_references_lvalue(prior_condition, target)) + }); + 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 } @@ -10517,6 +10555,64 @@ fn definitely_assigned_comb_targets( } } +fn guarded_comb_targets( + stmt: &sv_parser::StatementOrNull, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, +) -> 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)?; + if !statement_or_null_is_blocking_assignment(&conditional.nodes.3) { + return None; + } + let targets = definitely_assigned_comb_targets_statement_or_null( + &conditional.nodes.3, + syntax_tree, + packed_dimensions, + ); + if targets + .iter() + .any(|target| expr_references_lvalue(&condition, target)) + { + return None; + } + Some((condition, targets)) +} + +fn statement_or_null_is_blocking_assignment(stmt: &sv_parser::StatementOrNull) -> bool { + matches!( + stmt, + sv_parser::StatementOrNull::Statement(stmt) + if matches!(stmt.nodes.2, sv_parser::StatementItem::BlockingAssignment(_)) + ) +} + +fn two_state_conditions_are_complements( + left: &Expr, + right: &Expr, + packed_dimensions: &PackedDimensions, +) -> bool { + 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) + ) + }; + is_complement(left, right) || is_complement(right, left) +} + fn intersect_lvalue_sets( sets: Vec>, packed_dimensions: &PackedDimensions, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index d965b6df9..a2884be71 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3093,6 +3093,87 @@ fn recognizes_constant_true_nested_comb_guards_as_definite() { 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#" From f04a8d7cc6a1fa622a31aa1b8d88815af1b92b94 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 16:29:37 +0900 Subject: [PATCH 25/55] fix(sv-frontend): preserve deferred value semantics --- crates/celox-sv-analyzer/src/ast.rs | 26 ++++++- .../systemverilog/review_regressions.rs | 67 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 373a544c5..4b7af5528 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -195,7 +195,9 @@ impl Module { // 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. + // 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, @@ -203,6 +205,15 @@ impl Module { &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)?; } @@ -10210,7 +10221,13 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool 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)) + && 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, @@ -11218,7 +11235,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, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index a2884be71..838769e0c 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2350,6 +2350,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#" @@ -2928,6 +2974,27 @@ fn resolves_parameter_ranges_with_enum_constants() { assert_eq!(sim.get(sim.signal("y")), 0xffu8.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#" From 0547f0ff8b15340c42b1436e7a6c0d138436ce62 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 16:45:18 +0900 Subject: [PATCH 26/55] fix(sv-frontend): preserve procedural index semantics --- crates/celox-sv-analyzer/src/ast.rs | 49 +++++++++- .../systemverilog/review_regressions.rs | 89 +++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 4b7af5528..a9c84810e 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8190,8 +8190,15 @@ fn substitute_intermediate_comb_value_reads( if initialized || path_value.is_some() { let value = path_value.unwrap_or(&established); let assignment = guarded_assignment.assignment.clone(); - guarded_assignment.assignment = Assignment::new( + let lhs = substitute_comb_lvalue_reads( assignment.lhs_value().clone(), + target, + value, + whole_established.as_ref(), + packed_dimensions, + ); + guarded_assignment.assignment = Assignment::new( + lhs, substitute_comb_value_reads( assignment.rhs, target, @@ -8284,6 +8291,45 @@ fn substitute_comb_value_reads( 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, + )) + .unwrap_or(original) + }; + LValue::Select { + name, + msb: substitute_bound(msb), + lsb: substitute_bound(lsb), + signed, + array_slice_width, + array_slice_reversed, + } +} + fn simplify_single_bit_concat_selects(expr: Expr, packed_dimensions: &PackedDimensions) -> Expr { match expr { Expr::Select { @@ -10504,6 +10550,7 @@ fn definitely_assigned_comb_targets( ) { let condition = expr_from_cond_predicate(predicate, syntax_tree, packed_dimensions) + .map(procedural_truth_condition) .and_then(expr_to_const) .and_then(|condition| { eval_ast_const_expr(&condition, &packed_dimensions.const_env) diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 838769e0c..05ce4fa4c 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -179,6 +179,53 @@ fn substitutes_blocking_values_inside_dynamic_select_indices() { 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( @@ -3160,6 +3207,48 @@ fn recognizes_constant_true_nested_comb_guards_as_definite() { 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#" From 07f95745eeb32876fe621586f427fa5ef1320fb2 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 17:07:10 +0900 Subject: [PATCH 27/55] fix(sv-frontend): preserve enum parameter overrides --- crates/celox-sv-analyzer/src/ast.rs | 4 +++- .../systemverilog/review_regressions.rs | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index a9c84810e..03376a066 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -184,6 +184,7 @@ impl Module { syntax_tree, &const_env, &type_aliases, + applicable_parameter_overrides, )?; for (name, value) in &enum_constants.numbers { const_env.entry(name.clone()).or_insert(*value); @@ -3564,6 +3565,7 @@ fn enum_member_constants_from_module_node( 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(); @@ -3595,7 +3597,7 @@ fn enum_member_constants_from_module_node( false, &eval_env, type_aliases, - &HashMap::default(), + parameter_overrides, )?; extend_const_env_with_parameters(&mut eval_env, ¶meters); continue; diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 05ce4fa4c..2117cbd11 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3368,6 +3368,27 @@ fn collects_enum_constants_from_parameterized_aliased_bases() { assert_eq!(sim.get(sim.signal("y")), 2u8.into()); } +#[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#" From daea8c5425b8bf06033c14755cfc3db281940add Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 18:25:00 +0900 Subject: [PATCH 28/55] fix(sv-frontend): ignore unreachable case arms --- crates/celox-sv-analyzer/src/ast.rs | 158 ++++++++++++------ .../systemverilog/review_regressions.rs | 56 +++++++ 2 files changed, 159 insertions(+), 55 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 03376a066..ebb87d1eb 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -10332,72 +10332,112 @@ fn two_state_case_items_cover_selector( const_env: &HashMap, packed_dimensions: &PackedDimensions, ) -> bool { + two_state_case_item_reachability(stmt, syntax_tree, const_env, packed_dimensions) + .is_some_and(|(_, covered)| covered) +} + +fn two_state_case_item_reachability( + stmt: &sv_parser::CaseStatementNormal, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + packed_dimensions: &PackedDimensions, +) -> Option<(Vec, bool)> { if !matches!(&stmt.nodes.1, sv_parser::CaseKeyword::Case(_)) { - return false; + return None; } - let Some(selector) = expr_from_expression_with_types( + let selector = expr_from_expression_with_types( &stmt.nodes.2.nodes.1.nodes.0, syntax_tree, packed_dimensions, - ) else { - return false; - }; - let Some(width) = two_state_case_selector_width(&selector, packed_dimensions) else { - return false; - }; - let Some(value_count) = u32::try_from(width) + )?; + let width = two_state_case_selector_width(&selector, packed_dimensions)?; + let value_count = u32::try_from(width) .ok() - .and_then(|width| 1usize.checked_shl(width)) - else { - return false; - }; + .and_then(|width| 1usize.checked_shl(width))?; if i128::try_from(value_count).is_err() { - return false; + return None; } let identifiers = packed_dimensions .iter() .map(|(name, dimensions)| (name.clone(), dimensions.signed)) .collect::>(); - let Some(selector_signed) = expr_signedness(&selector, &identifiers, &HashMap::default()) - else { - return false; - }; - let mut labels = Vec::new(); + let selector_signed = expr_signedness(&selector, &identifiers, &HashMap::default())?; + 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()) { - let sv_parser::CaseItem::NonDefault(item) = item else { - continue; - }; - for label in item.nodes.0.contents() { - let Some(label) = - expr_from_expression_with_types(&label.nodes.0, syntax_tree, packed_dimensions) - else { - return false; - }; - if let Some(label) = expr_to_const(label) { - labels.push(label); + 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, + ) + .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); } } } - if labels.len() < value_count { - return false; + let label_count = labels_by_item + .iter() + .filter_map(Option::as_ref) + .map(Vec::len) + .sum::(); + if label_count < value_count { + return None; } - (0..value_count).all(|value| { + let mut reachable = vec![false; labels_by_item.len()]; + let mut covered = true; + for value in 0..value_count { let selector = ConstExpr::Literal(format_typed_parameter_literal( value as i128, width, selector_signed, )); - labels.iter().any(|label| { - eval_ast_const_expr( - &ConstExpr::Binary { - left: Box::new(selector.clone()), - op: BinaryOp::EqCase, - right: Box::new(label.clone()), - }, - const_env, - ) == Some(1) - }) - }) + 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; + } + } + if let Some(index) = matched { + reachable[index] = true; + } else { + covered = false; + if let Some(index) = default_index { + reachable[index] = true; + } + } + } + Some((reachable, covered)) } fn mark_condition_context( @@ -10586,10 +10626,25 @@ fn definitely_assigned_comb_targets( let sv_parser::CaseStatement::Normal(case) = &**case else { return Vec::new(); }; + let reachability = two_state_case_item_reachability( + case, + syntax_tree, + &packed_dimensions.const_env, + packed_dimensions, + ); + 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()) - .map(|item| { + .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) => { @@ -10597,21 +10652,14 @@ fn definitely_assigned_comb_targets( &item.nodes.2 } }; - definitely_assigned_comb_targets_statement_or_null( + Some(definitely_assigned_comb_targets_statement_or_null( branch, syntax_tree, packed_dimensions, - ) + )) }) .collect::>(); - if has_default - || two_state_case_items_cover_selector( - case, - syntax_tree, - &packed_dimensions.const_env, - packed_dimensions, - ) - { + if has_default || complete_two_state_case { intersect_lvalue_sets(branches, packed_dimensions) } else { Vec::new() diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 2117cbd11..7c86ed8f7 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3654,6 +3654,62 @@ fn recognizes_nested_complete_cases_as_definite_assignments() { 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#" From 9b10d6564c28c46b1f9ce37ab706d31d2c83d16a Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 18:54:19 +0900 Subject: [PATCH 29/55] fix(sv-frontend): preserve elaborated assignment semantics --- crates/celox-sv-analyzer/src/ast.rs | 181 ++++++++++++------ crates/celox-sv-analyzer/src/typecheck.rs | 68 +++++-- .../systemverilog/review_regressions.rs | 65 +++++++ 3 files changed, 243 insertions(+), 71 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index ebb87d1eb..a5b7fc5b0 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -872,14 +872,30 @@ fn constant_cast_const_expr( const_env, type_aliases, )?; - let operand_type = infer_const_expr_type(&operand, ¶meter_types_from_const_env(const_env))?; + 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 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 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 @@ -3615,11 +3631,16 @@ fn enum_member_constants_from_module_node( continue; }; let member_type = match &r#enum.nodes.1 { - Some(base) => type_from_ref_node(RefNode::EnumBaseType(base), syntax_tree) - .or_else(|| { - type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, type_aliases) - }) - .and_then(|r#type| expr_type_from_type(&r#type, &eval_env)), + Some(base) => type_from_ref_node_with_env( + RefNode::EnumBaseType(base), + syntax_tree, + &eval_env, + type_aliases, + ) + .or_else(|| { + type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, type_aliases) + }) + .and_then(|r#type| expr_type_from_type(&r#type, &eval_env)), None => Some(ExprType { width: 32, signed: true, @@ -7836,54 +7857,14 @@ fn comb_assignments_from_guarded( ) -> Result, AnalyzerError> { normalize_mixed_whole_selected_comb_writes(&mut guarded, packed_dimensions); let const_env = &packed_dimensions.const_env; - 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]); - } - } - } - // 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); - } - } + 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) { if indices .iter() @@ -7892,13 +7873,31 @@ fn comb_assignments_from_guarded( continue; } let initial = overlapping_value_before(&guarded, indices[0], target, packed_dimensions); - substitute_intermediate_comb_value_reads( + let (changed, chains) = substitute_intermediate_comb_value_reads( &mut guarded, indices, target, initial, packed_dimensions, )?; + 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; + } + } + (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 @@ -7953,6 +7952,65 @@ fn comb_assignments_from_guarded( Ok(slots.into_iter().flatten().collect()) } +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]); + } + } + } + (targets, groups) +} + +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, @@ -8137,7 +8195,7 @@ fn substitute_intermediate_comb_value_reads( target: &LValue, initial: Option, packed_dimensions: &PackedDimensions, -) -> Result<(), AnalyzerError> { +) -> 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); @@ -8145,6 +8203,8 @@ fn substitute_intermediate_comb_value_reads( 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, @@ -8199,6 +8259,11 @@ fn substitute_intermediate_comb_value_reads( whole_established.as_ref(), packed_dimensions, ); + if &lhs != assignment.lhs_value() { + 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( @@ -8272,7 +8337,7 @@ fn substitute_intermediate_comb_value_reads( } prior_target_writes.push((index, write.clone())); } - Ok(()) + Ok((lvalues_changed, changed_chains)) } fn substitute_comb_value_reads( diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index 758518c1f..5f2053532 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -146,6 +146,16 @@ 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) +} + pub fn substitute_typed_constants( expr: ConstExpr, constants: &HashMap, @@ -401,6 +411,20 @@ fn eval_four_state_binary( return None; } + let result = eval_four_state_binary_literal(left, op, right, signed)?; + integral_literal_as_i128(&result, signed) +} + +fn eval_four_state_binary_literal( + left: &IntegralLiteral, + op: BinaryOp, + right: &IntegralLiteral, + signed: bool, +) -> Option { + if !matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) { + return None; + } + let width = left.width; let width_mask = (BigUint::from(1u8) << width) - BigUint::from(1u8); let left_known = &width_mask ^ &left.mask; @@ -421,15 +445,12 @@ 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_truth(literal: &IntegralLiteral) -> Option { @@ -497,14 +518,35 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option op: UnaryOp::BitNot, expr, } => { - let ConstExpr::Literal(literal) = &**expr else { - return None; - }; - let mut literal = parse_integral_literal(literal)?; + let mut literal = integral_literal_from_const_expr(expr)?; let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - literal.value = width_mask ^ literal.value; + let known = &width_mask ^ &literal.mask; + literal.value = ((&width_mask ^ literal.value) & known) | &literal.mask; Some(literal) } + ConstExpr::Unary { + op: UnaryOp::ToTwoState, + expr, + } => { + let mut literal = integral_literal_from_const_expr(expr)?; + let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); + literal.value &= width_mask ^ &literal.mask; + literal.mask = BigUint::default(); + Some(literal) + } + ConstExpr::Binary { left, op, right } + if matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) => + { + let mut left = integral_literal_from_const_expr(left)?; + let mut right = integral_literal_from_const_expr(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) + } _ => None, } } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 7c86ed8f7..d4d71fe1a 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3368,6 +3368,44 @@ fn collects_enum_constants_from_parameterized_aliased_bases() { 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#" @@ -3537,6 +3575,33 @@ fn rejects_incomplete_cases_for_potentially_invalid_two_state_selects() { ); } +#[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#" From 7995a11b3ab7f83914767b1f543c945c66a3dc70 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 19:18:24 +0900 Subject: [PATCH 30/55] fix(sv-frontend): preserve typed coverage analysis --- crates/celox-sv-analyzer/src/ast.rs | 140 ++++++++++++++++-- .../systemverilog/review_regressions.rs | 118 +++++++++++++++ 2 files changed, 246 insertions(+), 12 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index a5b7fc5b0..83f675ef5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -256,7 +256,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)?; @@ -299,6 +299,18 @@ 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(), + ( + function.return_width, + function.return_signed, + function.return_is_2state, + ), + ) + })); for instance in &mut instances { for connection in &mut instance.port_connections { connection.actual_expr = connection.actual_expr.take().map(|expr| { @@ -3560,6 +3572,27 @@ fn coerce_const_parameter_value(value: i128, width: usize, signed: bool) -> i128 bits as i128 } } + +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; + } + 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)] @@ -3669,9 +3702,19 @@ fn enum_member_constants_from_module_node( type_aliases, ) .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` 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); @@ -4136,6 +4179,7 @@ struct PackedDimensions { variables: VariablePackedDimensions, const_env: HashMap, type_aliases: HashMap, + function_return_types: HashMap, bool, bool)>, } impl PackedDimensions { @@ -4148,6 +4192,7 @@ impl PackedDimensions { variables, const_env: const_env.clone(), type_aliases: type_aliases.clone(), + function_return_types: HashMap::default(), } } } @@ -8040,6 +8085,22 @@ fn normalize_mixed_whole_selected_comb_writes( } } 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; @@ -8048,7 +8109,38 @@ fn normalize_mixed_whole_selected_comb_writes( .get(&name) .is_some_and(|targets| lvalue_is_covered_by(&whole_target, targets, packed_dimensions)) { - whole_names.insert(name); + 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() { @@ -8056,27 +8148,38 @@ fn normalize_mixed_whole_selected_comb_writes( let LValue::Select { name, .. } = &target else { continue; }; - if !whole_names.contains(name) { - continue; - } - let Some(whole_target) = whole_packed_lvalue(name, packed_dimensions) else { + 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( - &Expr::Ident(name.clone()), - &whole_target, + ¤t, + normalization_target, &target, &write_value, packed_dimensions, ) else { continue; }; - write.assignment = Assignment::new(LValue::Ident(name.clone()), rhs); + write.assignment = Assignment::new(assignment_target.clone(), rhs); } // Several selected writes in one fallback branch become writes to the @@ -8584,7 +8687,10 @@ fn expr_static_width(expr: &Expr, packed_dimensions: &PackedDimensions) -> Optio expr_static_width(then_expr, packed_dimensions)? .max(expr_static_width(else_expr, packed_dimensions)?), ), - Expr::Call { .. } => None, + Expr::Call { name, .. } => packed_dimensions + .function_return_types + .get(name) + .and_then(|(width, _, _)| *width), } } @@ -10351,7 +10457,10 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool && expr_is_two_state(then_expr, packed_dimensions) && expr_is_two_state(else_expr, packed_dimensions) } - Expr::Call { .. } => false, + Expr::Call { name, .. } => packed_dimensions + .function_return_types + .get(name) + .is_some_and(|(_, _, is_2state)| *is_2state), } } @@ -10426,7 +10535,14 @@ fn two_state_case_item_reachability( .iter() .map(|(name, dimensions)| (name.clone(), dimensions.signed)) .collect::>(); - let selector_signed = expr_signedness(&selector, &identifiers, &HashMap::default())?; + let selector_signed = if let Expr::Call { name, .. } = &selector { + packed_dimensions + .function_return_types + .get(name) + .map(|(_, signed, _)| *signed)? + } else { + expr_signedness(&selector, &identifiers, &HashMap::default())? + }; 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()) { diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index d4d71fe1a..3c9c0de82 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3555,6 +3555,42 @@ fn recognizes_complete_cases_over_two_state_selectors() { assert_eq!(sim.get(y), 1u8.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 rejects_incomplete_cases_for_potentially_invalid_two_state_selects() { let error = cranelift_build_error( @@ -3810,6 +3846,22 @@ fn rejects_ranged_enum_members_instead_of_registering_the_base_name() { ); } +#[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#" @@ -3960,6 +4012,72 @@ fn merges_exhaustive_writes_across_different_slice_partitions() { 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( From 289384340de4697176c0a83aa88d86ffae606426 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 19:35:47 +0900 Subject: [PATCH 31/55] fix(sv-frontend): handle unreachable cases and enum calls --- crates/celox-sv-analyzer/src/ast.rs | 35 ++++++----- .../systemverilog/review_regressions.rs | 58 +++++++++++++++++++ 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 83f675ef5..e1dad2e04 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -314,7 +314,12 @@ impl Module { 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, + ) }); } } @@ -10299,12 +10304,24 @@ fn conditional_assignments_from_case_statement( packed_dimensions, ) .ok_or_else(|| AnalyzerError::Unsupported("always_ff case selector lowering".to_string()))?; - let complete_two_state_case = - two_state_case_items_cover_selector(stmt, syntax_tree, const_env, packed_dimensions); + 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 mut branches = Vec::new(); let mut default_branch = None; - for item in std::iter::once(&stmt.nodes.3).chain(stmt.nodes.4.iter()) { + 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]) + { + continue; + } match item { sv_parser::CaseItem::NonDefault(item) => { let mut conditions = Vec::new(); @@ -10500,16 +10517,6 @@ fn select_bounds_are_statically_valid( msb.min(lsb) >= valid_low && msb.max(lsb) <= valid_high } -fn two_state_case_items_cover_selector( - stmt: &sv_parser::CaseStatementNormal, - syntax_tree: &SyntaxTree, - const_env: &HashMap, - packed_dimensions: &PackedDimensions, -) -> bool { - two_state_case_item_reachability(stmt, syntax_tree, const_env, packed_dimensions) - .is_some_and(|(_, covered)| covered) -} - fn two_state_case_item_reachability( stmt: &sv_parser::CaseStatementNormal, syntax_tree: &SyntaxTree, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 3c9c0de82..0d3c4b7e5 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3591,6 +3591,40 @@ fn recognizes_complete_cases_over_two_state_function_results() { 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( @@ -3830,6 +3864,30 @@ fn registers_enum_types_with_default_bases() { 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( From 43807bea2c4792a3f6d652e6579beee17df1691a Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 19:59:07 +0900 Subject: [PATCH 32/55] fix(sv-frontend): address size and case review feedback --- crates/celox-sv-analyzer/src/ast.rs | 112 +++++++++++++----- .../systemverilog/review_regressions.rs | 90 ++++++++++++++ 2 files changed, 173 insertions(+), 29 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index e1dad2e04..df592602c 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -740,7 +740,12 @@ fn size_system_function_expr_type( identifier_text(RefNode::TypeIdentifier(&data_type.nodes.1), syntax_tree)?; type_aliases.get(&name).cloned() } - _ => type_from_ref_node(RefNode::DataType(data_type), syntax_tree), + _ => type_from_ref_node_with_env( + RefNode::DataType(data_type), + syntax_tree, + const_env, + type_aliases, + ), }?; (name, r#type) } @@ -754,10 +759,22 @@ fn size_system_function_expr_type( return None; } let argument = arguments[0].as_ref()?; - let ConstExpr::Ident(alias) = const_expr_from_expr(argument, syntax_tree)? else { - return None; - }; - (name, type_aliases.get(&alias)?.clone()) + 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 name != "$bits" && name != "$size" { + return None; + } + 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, }; @@ -3386,7 +3403,19 @@ fn parameters_from_ref_node( type_aliases: &HashMap, parameter_overrides: &HashMap, ) -> Result<(), AnalyzerError> { - if node.clone().into_iter().any(|child| { + // 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)) @@ -3403,15 +3432,15 @@ fn parameters_from_ref_node( "unsupported parameter data type".to_string(), )); } - let declared_alias = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases); + let declared_alias = type_alias_from_ref_node(type_node.clone(), syntax_tree, type_aliases); let parameter_width = parameter_declared_width( - node.clone(), + type_node.clone(), syntax_tree, base_const_env, parameters, type_aliases, ); - let has_declared_type = node.clone().into_iter().any(|child| { + let has_declared_type = type_node.clone().into_iter().any(|child| { matches!( child, RefNode::DataTypeOrImplicit(sv_parser::DataTypeOrImplicit::DataType(_)) @@ -3422,13 +3451,13 @@ fn parameters_from_ref_node( .as_ref() .map(Type::is_signed) .unwrap_or_else(|| { - integer_atom_expr_type(node.clone()) + integer_atom_expr_type(type_node.clone()) .map(|r#type| r#type.signed) - .unwrap_or_else(|| is_signed_from_ref_node(node.clone()).unwrap_or(false)) + .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(node.clone(), syntax_tree)) + .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 { @@ -8270,6 +8299,9 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap Option { match expr { Expr::Ident(name) => lvalue_expr_type(&LValue::Ident(name.clone()), packed_dimensions) - .map(|r#type| r#type.width), + .map(|r#type| r#type.width) + .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) } @@ -10438,9 +10475,12 @@ fn two_state_case_selector_width( fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool { match expr { - Expr::Ident(name) => packed_dimensions - .get(name) - .is_some_and(|dimensions| dimensions.is_2state), + 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, .. } => { @@ -10470,9 +10510,10 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool then_expr, else_expr, } => { - expr_is_two_state(condition, packed_dimensions) - && expr_is_two_state(then_expr, packed_dimensions) - && expr_is_two_state(else_expr, packed_dimensions) + 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 @@ -10531,17 +10572,30 @@ fn two_state_case_item_reachability( syntax_tree, packed_dimensions, )?; + let selector = simplify_constant_mux_conditions(selector, const_env); let width = two_state_case_selector_width(&selector, packed_dimensions)?; - let value_count = u32::try_from(width) - .ok() - .and_then(|width| 1usize.checked_shl(width))?; - if i128::try_from(value_count).is_err() { - return None; - } - let identifiers = packed_dimensions + let selector_values = if let Some(value) = expr_to_const(selector.clone()) + .and_then(|selector| eval_ast_const_expr(&selector, const_env)) + { + vec![value] + } else { + let value_count = u32::try_from(width) + .ok() + .and_then(|width| 1usize.checked_shl(width))?; + if i128::try_from(value_count).is_err() { + return None; + } + (0..value_count).map(|value| value as i128).collect() + }; + 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 @@ -10582,14 +10636,14 @@ fn two_state_case_item_reachability( .filter_map(Option::as_ref) .map(Vec::len) .sum::(); - if label_count < value_count { + if label_count < selector_values.len() { return None; } let mut reachable = vec![false; labels_by_item.len()]; let mut covered = true; - for value in 0..value_count { + for value in selector_values { let selector = ConstExpr::Literal(format_typed_parameter_literal( - value as i128, + value, width, selector_signed, )); diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 0d3c4b7e5..19d9d4b6c 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2876,6 +2876,42 @@ fn resolves_typedefs_in_size_function_cast_targets() { 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 recognizes_exhaustive_comb_coverage_across_selected_writes() { let source = r#" @@ -3555,6 +3591,60 @@ fn recognizes_complete_cases_over_two_state_selectors() { 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#" From 88ecf37f2e6ac49bc1a410d56081a225cd597eca Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 20:37:43 +0900 Subject: [PATCH 33/55] fix(sv-frontend): resolve variable size cast targets --- crates/celox-sv-analyzer/src/ast.rs | 148 +++++++++++++++++- .../systemverilog/review_regressions.rs | 33 ++++ 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index df592602c..624bc0bfe 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -221,7 +221,67 @@ impl Module { 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)?; + + 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())) { @@ -768,6 +828,15 @@ fn size_system_function_expr_type( if name != "$bits" && name != "$size" { return None; } + 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 { @@ -4385,6 +4454,83 @@ fn parameter_signed_marker(name: &str) -> String { format!("__parameter::signed::{name}") } +fn variable_bits_marker(name: &str) -> String { + format!("__variable::bits::{name}") +} + +fn variable_size_marker(name: &str) -> String { + format!("__variable::size::{name}") +} + +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); + } +} + fn insert_parameter_type_markers( const_env: &mut HashMap, name: &str, diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 19d9d4b6c..82be5f2d8 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -2912,6 +2912,39 @@ fn infers_parameter_expression_widths_in_size_function_cast_targets() { 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#" From 0f9331b79c7f510c3eb0bfc67f95f795ec384fbd Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 22:03:04 +0900 Subject: [PATCH 34/55] fix(sv-frontend): preserve typed expression semantics --- crates/celox-sv-analyzer/src/ast.rs | 74 +++++++++++++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 46 ++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 624bc0bfe..fa6d41bfc 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -819,15 +819,28 @@ fn size_system_function_expr_type( 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 name != "$bits" && name != "$size" { - return None; - } if let ConstExpr::Ident(identifier) = &argument && let Some(width) = variable_size_function_width(const_env, identifier, name == "$size") @@ -900,6 +913,44 @@ fn size_system_function_expr_type( }) } +fn size_function_expression_type( + argument: &sv_parser::Expression, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, + first_dimension_only: bool, +) -> Option { + let packed_dimensions = PackedDimensions { + const_env: const_env.clone(), + type_aliases: type_aliases.clone(), + ..PackedDimensions::default() + }; + let expression = expr_from_expression_with_types(argument, syntax_tree, &packed_dimensions)?; + let width = if first_dimension_only { + match &expression { + Expr::Ident(name) => variable_size_function_width(const_env, name, true), + _ => expr_static_width(&expression, &packed_dimensions), + } + } else { + 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(&expression, &identifier_signedness, &HashMap::default())?; + Some(ExprType { + width: width.max(1), + signed, + }) +} + fn expr_type_from_type(r#type: &Type, const_env: &HashMap) -> Option { if !r#type.unpacked_ranges().is_empty() { return None; @@ -8091,12 +8142,9 @@ fn comb_assignments_from_guarded( let mut lvalues_changed = false; let mut changed_chains = HashSet::default(); for (target, indices) in targets.iter().zip(&groups) { - if indices + let preserve_target_writes = indices .iter() - .all(|index| guarded[*index].condition().is_none()) - { - continue; - } + .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, @@ -8104,6 +8152,7 @@ fn comb_assignments_from_guarded( target, initial, packed_dimensions, + preserve_target_writes, )?; lvalues_changed |= changed; changed_chains.extend(chains); @@ -8481,6 +8530,7 @@ fn substitute_intermediate_comb_value_reads( target: &LValue, initial: Option, packed_dimensions: &PackedDimensions, + preserve_target_writes: bool, ) -> Result<(bool, HashSet), AnalyzerError> { let first = *indices.first().expect("group is non-empty"); let mut initialized = initial.is_some(); @@ -8501,6 +8551,8 @@ fn substitute_intermediate_comb_value_reads( 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 @@ -8545,7 +8597,7 @@ fn substitute_intermediate_comb_value_reads( whole_established.as_ref(), packed_dimensions, ); - if &lhs != assignment.lhs_value() { + 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); @@ -8622,6 +8674,9 @@ fn substitute_intermediate_comb_value_reads( } } prior_target_writes.push((index, write.clone())); + if let Some(assignment) = original_target_assignment { + guarded_assignment.assignment = assignment; + } } Ok((lvalues_changed, changed_chains)) } @@ -8806,6 +8861,7 @@ fn expr_static_width(expr: &Expr, packed_dimensions: &PackedDimensions) -> Optio 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) diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 0496a3755..b20598195 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -218,6 +218,36 @@ mod tests { 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( @@ -662,6 +692,22 @@ mod tests { 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( From a88bc020923590cb90fb5ad80805d47dea9300b0 Mon Sep 17 00:00:00 2001 From: tignear Date: Sat, 29 Aug 2026 22:07:13 +0900 Subject: [PATCH 35/55] fix(sv-frontend): complete enum and case analysis --- crates/celox-sv-analyzer/src/ast.rs | 63 ++++++++++++++++++++++++----- crates/celox-sv-analyzer/src/lib.rs | 60 +++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index fa6d41bfc..3ffc6ad34 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -3772,7 +3772,8 @@ fn enum_member_constants_from_module_node( ) -> Result { let mut constants = EnumMemberConstants::default(); let mut eval_env = base_const_env.clone(); - for item in module_non_port_items(node) { + 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; }; @@ -3785,10 +3786,12 @@ fn enum_member_constants_from_module_node( &mut parameters, true, &eval_env, - type_aliases, + &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) => { @@ -3799,10 +3802,12 @@ fn enum_member_constants_from_module_node( &mut parameters, false, &eval_env, - type_aliases, + &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, @@ -3822,10 +3827,14 @@ fn enum_member_constants_from_module_node( RefNode::EnumBaseType(base), syntax_tree, &eval_env, - type_aliases, + &resolved_type_aliases, ) .or_else(|| { - type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, type_aliases) + 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 { @@ -3853,7 +3862,7 @@ fn enum_member_constants_from_module_node( RefNode::ConstantExpression(value), syntax_tree, &eval_env, - type_aliases, + &resolved_type_aliases, ) .ok_or_else(|| AnalyzerError::Unsupported(format!("enum member `{name}` value")))?; let value_type = @@ -3884,6 +3893,11 @@ fn enum_member_constants_from_module_node( )), ); } + // 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) } @@ -8497,6 +8511,9 @@ fn simplify_constant_mux_conditions(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 @@ -10838,7 +10882,7 @@ fn two_state_case_item_reachability( .filter_map(Option::as_ref) .map(Vec::len) .sum::(); - if label_count < selector_values.len() { + if default_index.is_none() && label_count < selector_values.len() { return None; } let mut reachable = vec![false; labels_by_item.len()]; @@ -10874,11 +10918,10 @@ fn two_state_case_item_reachability( } if let Some(index) = matched { reachable[index] = true; + } else if let Some(index) = default_index { + reachable[index] = true; } else { covered = false; - if let Some(index) = default_index { - reachable[index] = true; - } } } Some((reachable, covered)) diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index b20598195..1c593657f 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -971,6 +971,66 @@ mod tests { 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 preserves_named_constant_casts_in_packed_ranges() { let ir = analyze_source( From c910310c02ac89e468c7b374f8f3f6f9fce0cd5e Mon Sep 17 00:00:00 2001 From: tignear Date: Sun, 30 Aug 2026 05:55:20 +0900 Subject: [PATCH 36/55] fix(sv-frontend): preserve wide case and loop semantics --- crates/celox-sv-analyzer/src/ast.rs | 255 ++++++++++++++---- .../systemverilog/review_regressions.rs | 124 ++++++++- 2 files changed, 326 insertions(+), 53 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 3ffc6ad34..572837d90 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -1335,6 +1335,19 @@ fn reject_silently_ignored_constructs( 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!( @@ -1352,16 +1365,6 @@ fn reject_silently_ignored_constructs( "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() @@ -7565,6 +7568,15 @@ 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, bool, bool)>, ) -> Option { match expr { Expr::Ident(name) => identifiers.get(name).copied(), @@ -7581,7 +7593,12 @@ fn expr_signedness( ) { Some(false) } else { - expr_signedness(expr, identifiers, functions) + expr_signedness_with_return_types( + expr, + identifiers, + functions, + function_return_types, + ) } } Expr::Binary { left, op, right } => { @@ -7602,11 +7619,25 @@ fn expr_signedness( ) { Some(false) } else if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar) { - expr_signedness(left, identifiers, functions) + expr_signedness_with_return_types( + left, + identifiers, + functions, + function_return_types, + ) } else { Some( - expr_signedness(left, identifiers, functions)? - && expr_signedness(right, identifiers, functions)?, + expr_signedness_with_return_types( + left, + identifiers, + functions, + function_return_types, + )? && expr_signedness_with_return_types( + right, + identifiers, + functions, + function_return_types, + )?, ) } } @@ -7615,10 +7646,26 @@ fn expr_signedness( else_expr, .. } => Some( - expr_signedness(then_expr, identifiers, functions)? - && expr_signedness(else_expr, identifiers, functions)?, + 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), + Expr::Call { name, .. } => functions + .get(name) + .map(|function| function.return_signed) + .or_else(|| { + function_return_types + .get(name) + .map(|(_, signed, _)| *signed) + }), } } @@ -8668,6 +8715,34 @@ fn substitute_intermediate_comb_value_reads( } 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. + 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(), + guarded_assignment.assignment().rhs(), + 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; } write_index += 1; @@ -9852,6 +9927,14 @@ fn validate_always_comb_statement(stmt: &sv_parser::Statement) -> Result<(), Ana } 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(), )), @@ -10383,8 +10466,12 @@ fn coerce_procedural_assignment_rhs( .iter() .map(|(name, dimensions)| (name.clone(), dimensions.signed)) .collect(); - let Some(source_signed) = expr_signedness(&rhs, &identifier_signedness, &HashMap::default()) - else { + 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 @@ -10592,6 +10679,12 @@ fn conditional_assignments_from_case_statement( 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; @@ -10602,6 +10695,7 @@ fn conditional_assignments_from_case_statement( if item_reachability .as_ref() .is_some_and(|(reachable, _)| !reachable[item_index]) + && !preserve_unmatched_writes { continue; } @@ -10820,19 +10914,14 @@ fn two_state_case_item_reachability( )?; let selector = simplify_constant_mux_conditions(selector, const_env); let width = two_state_case_selector_width(&selector, packed_dimensions)?; - let selector_values = if let Some(value) = expr_to_const(selector.clone()) - .and_then(|selector| eval_ast_const_expr(&selector, const_env)) - { - vec![value] - } else { - let value_count = u32::try_from(width) - .ok() - .and_then(|width| 1usize.checked_shl(width))?; - if i128::try_from(value_count).is_err() { - return None; - } - (0..value_count).map(|value| value as i128).collect() - }; + // Constant evaluation and the type model use i128 values. Wider dynamic + // selectors still lower normally, but are not candidates for this + // reachability optimization. + if width > 128 { + return None; + } + let constant_selector_value = expr_to_const(selector.clone()) + .and_then(|selector| eval_ast_const_expr(&selector, const_env)); let mut identifiers = packed_dimensions .iter() .map(|(name, dimensions)| (name.clone(), dimensions.signed)) @@ -10877,17 +10966,8 @@ fn two_state_case_item_reachability( } } } - let label_count = labels_by_item - .iter() - .filter_map(Option::as_ref) - .map(Vec::len) - .sum::(); - if default_index.is_none() && label_count < selector_values.len() { - return None; - } let mut reachable = vec![false; labels_by_item.len()]; - let mut covered = true; - for value in selector_values { + if let Some(value) = constant_selector_value { let selector = ConstExpr::Literal(format_typed_parameter_literal( value, width, @@ -10916,15 +10996,65 @@ fn two_state_case_item_reachability( break; } } - if let Some(index) = matched { + let covered = if let Some(index) = matched { reachable[index] = true; + true } else if let Some(index) = default_index { reachable[index] = true; + true } else { - covered = false; + false + }; + return Some((reachable, covered)); + } + + // 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; + } } } - Some((reachable, covered)) + 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 mark_condition_context( @@ -11152,6 +11282,41 @@ fn definitely_assigned_comb_targets( Vec::new() } } + sv_parser::StatementItem::LoopStatement(loop_statement) => { + 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 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 definitely_assigned_comb_targets_statement_or_null( + &loop_statement.nodes.2, + syntax_tree, + &iteration_dimensions, + ) { + if !targets.contains(&target) { + targets.push(target); + } + } + } + targets + } _ => Vec::new(), } } diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 82be5f2d8..e7410a37d 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -5747,14 +5747,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#" @@ -7604,6 +7596,122 @@ 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()); +} + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] #[test] fn collapses_unknown_initializers_in_two_state_native_images() { From d5ecd9f3f824b13809b2d00a10e61d6d609ec0c9 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 04:28:29 +0900 Subject: [PATCH 37/55] fix(sv-frontend): coerce tracked comb assignments --- crates/celox-sv-analyzer/src/ast.rs | 38 ++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 572837d90..c380926c9 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8718,6 +8718,14 @@ fn substitute_intermediate_comb_value_reads( // 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()), @@ -8727,7 +8735,7 @@ fn substitute_intermediate_comb_value_reads( &established, &tracked_target, guarded_assignment.assignment().lhs_value(), - guarded_assignment.assignment().rhs(), + &write_value, packed_dimensions, ) { @@ -8747,7 +8755,14 @@ fn substitute_intermediate_comb_value_reads( } write_index += 1; let write = &*guarded_assignment; - let value = write.assignment().rhs().clone(); + 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( @@ -8768,9 +8783,11 @@ fn substitute_intermediate_comb_value_reads( }); } if let Some(chain_start) = write.exhaustive_fallback_start { - established = value; + established = value.clone(); for (prior_index, prior_write) in &prior_target_writes { - if *prior_index >= chain_start && prior_write.path_epochs != write.path_epochs { + if *prior_index >= chain_start + && prior_write.path_epochs != tracked_write.path_epochs + { established = fold_conditional_assignment_over(established, prior_write); } } @@ -8779,20 +8796,23 @@ fn substitute_intermediate_comb_value_reads( if write.condition().is_none() { initialized = true; } - established = fold_conditional_assignment_over(established, write); + established = fold_conditional_assignment_over(established, &tracked_write); } - for (depth, epoch) in write.path_epochs.iter().enumerate() { + for (depth, epoch) in tracked_write.path_epochs.iter().enumerate() { if depth == 0 { - path_values.insert(*epoch, write.assignment().rhs().clone()); + 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, write)); + path_values.insert( + *epoch, + fold_conditional_assignment_over(current, &tracked_write), + ); } } - prior_target_writes.push((index, write.clone())); + prior_target_writes.push((index, tracked_write)); if let Some(assignment) = original_target_assignment { guarded_assignment.assignment = assignment; } From bc24c7e06be6fec126dd9541dc88627e7cf88ce8 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 04:36:28 +0900 Subject: [PATCH 38/55] fix(sv-frontend): address remaining review findings --- crates/celox-sv-analyzer/src/ast.rs | 35 +++++++++++++++- crates/celox-sv-analyzer/src/lib.rs | 64 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index c380926c9..56d51940a 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -1130,7 +1130,14 @@ fn casting_type_is_numeric_size( type_aliases: &HashMap, ) -> bool { match casting_type { - sv_parser::CastingType::ConstantPrimary(_) => true, + 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; @@ -3868,6 +3875,17 @@ fn enum_member_constants_from_module_node( &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(|| { @@ -8640,6 +8658,21 @@ fn substitute_intermediate_comb_value_reads( 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_lvalue(condition, target)) + || expr_references_lvalue(assignment.assignment().rhs(), target) + }) { + return Err(AnalyzerError::Unsupported( + "read-before-write dependency inside always_comb".to_string(), + )); + } + 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) diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 1c593657f..1d367d331 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -579,6 +579,33 @@ mod tests { 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}" + ); + } + #[test] fn preserves_prior_partially_overlapping_selected_writes() { let ir = analyze_source( @@ -801,6 +828,26 @@ mod tests { ); } + #[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( @@ -1090,6 +1137,23 @@ mod tests { 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( From df4a27ebd0b90a758232b8a546b9f3d7942b400d Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 05:31:01 +0900 Subject: [PATCH 39/55] fix(sv-frontend): handle review edge cases --- crates/celox-sv-analyzer/src/ast.rs | 173 ++++++++++++++++++++++------ crates/celox-sv-analyzer/src/lib.rs | 57 +++++++++ 2 files changed, 192 insertions(+), 38 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 56d51940a..5ad7d1023 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -3569,6 +3569,7 @@ fn parameters_from_ref_node( base_const_env, parameters, type_aliases, + parameter_overrides, ); let has_declared_type = type_node.clone().into_iter().any(|child| { matches!( @@ -3626,20 +3627,42 @@ fn parameter_declared_width( 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 ranges = declared_alias - .as_ref() - .map(|r#type| r#type.packed_ranges.clone()) - .unwrap_or_else(|| packed_ranges_from_ref_node(node.clone(), syntax_tree)); - 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); + 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); } - return unwrap_node!(node, IntegerVectorType).is_some().then_some(1); } let mut env = base_const_env.clone(); // A second lowering pass receives values from the first pass in the base @@ -3657,6 +3680,26 @@ fn parameter_declared_width( } } env.extend(const_env_from_parameters(parameters)); + let ranges = declared_alias + .as_ref() + .map(|r#type| r#type.packed_ranges.clone()) + .unwrap_or_else(|| { + packed_ranges_from_ref_node_with_env( + node.clone(), + syntax_tree, + &range_env, + type_aliases, + ) + }); + 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)?; @@ -10966,32 +11009,6 @@ fn two_state_case_item_reachability( packed_dimensions, )?; let selector = simplify_constant_mux_conditions(selector, const_env); - let width = two_state_case_selector_width(&selector, packed_dimensions)?; - // Constant evaluation and the type model use i128 values. Wider dynamic - // selectors still lower normally, but are not candidates for this - // reachability optimization. - if width > 128 { - return None; - } - let constant_selector_value = expr_to_const(selector.clone()) - .and_then(|selector| eval_ast_const_expr(&selector, const_env)); - 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(|(_, signed, _)| *signed)? - } else { - expr_signedness(&selector, &identifiers, &HashMap::default())? - }; 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()) { @@ -11019,6 +11036,35 @@ fn two_state_case_item_reachability( } } } + let (duplicate_reachability, has_duplicate) = case_item_duplicate_reachability(&labels_by_item); + let Some(width) = two_state_case_selector_width(&selector, packed_dimensions) else { + 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 = expr_to_const(selector.clone()) + .and_then(|selector| eval_ast_const_expr(&selector, const_env)); + 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(|(_, signed, _)| *signed)? + } else { + expr_signedness(&selector, &identifiers, &HashMap::default())? + }; let mut reachable = vec![false; labels_by_item.len()]; if let Some(value) = constant_selector_value { let selector = ConstExpr::Literal(format_typed_parameter_literal( @@ -11110,6 +11156,27 @@ fn two_state_case_item_reachability( } } +fn case_item_duplicate_reachability( + labels_by_item: &[Option>], +) -> (Vec, bool) { + 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.contains(&label); + has_duplicate |= duplicate; + reachable[index] |= !duplicate; + prior_labels.push(label); + } + } + (reachable, has_duplicate) +} + fn mark_condition_context( assignments: &mut [ConditionalAssignment], start: usize, @@ -11429,7 +11496,37 @@ fn two_state_conditions_are_complements( } if &**expr == other && expr_is_two_state(other, packed_dimensions) ) }; - is_complement(left, right) || is_complement(right, left) + 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 intersect_lvalue_sets( diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 1d367d331..7b20dcc65 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -329,6 +329,26 @@ mod tests { ); } + #[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 substitutes_reads_of_selected_comb_targets() { let ir = analyze_source( @@ -1078,6 +1098,25 @@ mod tests { .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 preserves_named_constant_casts_in_packed_ranges() { let ir = analyze_source( @@ -1098,6 +1137,24 @@ mod tests { ); } + #[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( From f4eec2a95912ca9482aa212c73f991e85a2a947b Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 05:58:41 +0900 Subject: [PATCH 40/55] fix(sv-frontend): cover additional review cases --- crates/celox-sv-analyzer/src/ast.rs | 262 +++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 145 ++++++++++ .../systemverilog/review_regressions.rs | 19 ++ 3 files changed, 387 insertions(+), 39 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 5ad7d1023..a2c478192 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -551,10 +551,9 @@ 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)?; 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 { @@ -596,6 +595,17 @@ fn static_for_loop_iterations( (eval_ast_const_expr(&condition, &loop_env) == Some(0)).then_some((name, values)) } +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; @@ -6069,8 +6079,13 @@ fn value_type_from_data_type( 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 r#type = type_from_ref_node_with_env( + RefNode::DataType(node), + syntax_tree, + const_env, + type_aliases, + ) + .or_else(|| type_alias_from_data_type(node, syntax_tree, type_aliases))?; let width = if r#type.packed_ranges().is_empty() { 1 } else { @@ -6105,7 +6120,12 @@ fn value_type_from_ref_node( let ranges = if let Some(alias) = &alias { alias.packed_ranges() } else { - direct_ranges = packed_ranges_from_ref_node(node.clone(), syntax_tree); + direct_ranges = packed_ranges_from_ref_node_with_env( + node.clone(), + syntax_tree, + const_env, + type_aliases, + ); &direct_ranges }; let width = if ranges.is_empty() { @@ -8212,6 +8232,7 @@ fn comb_process_from_always_construct( &always.nodes.1, None, true, + true, syntax_tree, &packed_dimensions.const_env, packed_dimensions, @@ -8614,8 +8635,31 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap { let condition = simplify_constant_mux_conditions(*condition, const_env); - let then_expr = simplify_constant_mux_conditions(*then_expr, const_env); - let else_expr = simplify_constant_mux_conditions(*else_expr, 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); + // 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; } @@ -8645,6 +8689,32 @@ fn simplify_constant_mux_conditions(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 equivalent_constant_mux_value( then_expr: &Expr, else_expr: &Expr, @@ -10268,6 +10338,7 @@ fn ff_process_from_always_construct( body, None, false, + false, syntax_tree, const_env, packed_dimensions, @@ -10359,6 +10430,7 @@ 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, @@ -10369,6 +10441,7 @@ fn conditional_assignments_from_statement_or_null( stmt, condition, exhaustive_fallback, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10382,6 +10455,7 @@ 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, @@ -10466,6 +10540,7 @@ fn conditional_assignments_from_statement( stmt, condition.clone(), exhaustive_fallback, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10478,6 +10553,7 @@ fn conditional_assignments_from_statement( stmt, condition, exhaustive_fallback, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10489,6 +10565,7 @@ fn conditional_assignments_from_statement( stmt, condition, exhaustive_fallback, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10504,7 +10581,22 @@ fn conditional_assignments_from_statement( sv_parser::LoopStatement::For(loop_statement) => &loop_statement.nodes.2, _ => unreachable!(), }; - for value in values { + 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(); @@ -10519,10 +10611,16 @@ fn conditional_assignments_from_statement( ); 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, - condition.clone(), - exhaustive_fallback, + iteration_condition, + exhaustive_fallback && reachable, + retain_unreachable_writes, syntax_tree, &loop_const_env, &loop_packed_dimensions, @@ -10638,6 +10736,7 @@ 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, packed_dimensions: &PackedDimensions, @@ -10660,6 +10759,7 @@ fn conditional_assignments_from_conditional_statement( &stmt.nodes.3, then_condition, false, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10687,6 +10787,7 @@ fn conditional_assignments_from_conditional_statement( branch, condition, false, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10711,6 +10812,7 @@ fn conditional_assignments_from_conditional_statement( branch, condition.clone(), false, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10748,6 +10850,7 @@ 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, @@ -10839,6 +10942,7 @@ fn conditional_assignments_from_case_statement( branch, condition, false, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10878,6 +10982,7 @@ fn conditional_assignments_from_case_statement( branch, condition.clone(), false, + retain_unreachable_writes, syntax_tree, const_env, packed_dimensions, @@ -10901,14 +11006,6 @@ fn conditional_assignments_from_case_statement( Ok(()) } -fn two_state_case_selector_width( - selector: &Expr, - packed_dimensions: &PackedDimensions, -) -> Option { - expr_is_two_state(selector, packed_dimensions) - .then(|| expr_static_width(selector, packed_dimensions))? -} - fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool { match expr { Expr::Ident(name) => { @@ -11036,18 +11133,11 @@ fn two_state_case_item_reachability( } } } - let (duplicate_reachability, has_duplicate) = case_item_duplicate_reachability(&labels_by_item); - let Some(width) = two_state_case_selector_width(&selector, packed_dimensions) else { + let (duplicate_reachability, has_duplicate) = + case_item_duplicate_reachability(&labels_by_item, const_env); + let Some(width) = expr_static_width(&selector, packed_dimensions) else { 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 = expr_to_const(selector.clone()) - .and_then(|selector| eval_ast_const_expr(&selector, const_env)); let mut identifiers = packed_dimensions .iter() .map(|(name, dimensions)| (name.clone(), dimensions.signed)) @@ -11061,10 +11151,33 @@ fn two_state_case_item_reachability( packed_dimensions .function_return_types .get(name) - .map(|(_, signed, _)| *signed)? + .map(|(_, signed, _)| *signed) } else { - expr_signedness(&selector, &identifiers, &HashMap::default())? + expr_signedness(&selector, &identifiers, &HashMap::default()) + }; + let Some(selector_signed) = selector_signed else { + return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); }; + 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 = expr_to_const(selector.clone()) + .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( @@ -11156,8 +11269,69 @@ fn two_state_case_item_reachability( } } +fn finite_four_state_case_item_reachability( + labels_by_item: &[Option>], + default_index: Option, + width: usize, + selector_signed: bool, + const_env: &HashMap, +) -> 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, + )?; + if equal != 0 { + matched = Some(index); + break; + } + } + if matched.is_some() { + break; + } + } + if let Some(index) = matched.or(default_index) { + reachable[index] = true; + } else { + covered = false; + } + } + Some((reachable, covered)) +} + fn case_item_duplicate_reachability( labels_by_item: &[Option>], + const_env: &HashMap, ) -> (Vec, bool) { let mut reachable = vec![false; labels_by_item.len()]; let mut prior_labels: Vec<&ConstExpr> = Vec::new(); @@ -11168,7 +11342,17 @@ fn case_item_duplicate_reachability( continue; }; for label in labels { - let duplicate = prior_labels.contains(&label); + let duplicate = prior_labels.iter().any(|prior| { + *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); @@ -11430,6 +11614,8 @@ fn definitely_assigned_comb_targets( syntax_tree, &iteration_dimensions, ) { + let target = + substitute_lvalue_constants(target, &iteration_dimensions.const_env); if !targets.contains(&target) { targets.push(target); } @@ -11457,17 +11643,15 @@ fn guarded_comb_targets( } let condition = expr_from_cond_predicate(&conditional.nodes.2.nodes.1, syntax_tree, packed_dimensions)?; - if !statement_or_null_is_blocking_assignment(&conditional.nodes.3) { - return None; - } let targets = definitely_assigned_comb_targets_statement_or_null( &conditional.nodes.3, syntax_tree, packed_dimensions, ); - if targets - .iter() - .any(|target| expr_references_lvalue(&condition, target)) + if targets.is_empty() + || targets + .iter() + .any(|target| expr_references_lvalue(&condition, target)) { return None; } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 7b20dcc65..30f3e8cde 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -349,6 +349,26 @@ mod tests { .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 substitutes_reads_of_selected_comb_targets() { let ir = analyze_source( @@ -1117,6 +1137,131 @@ mod tests { .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"); + } + + #[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 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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index e7410a37d..a5d26b0b5 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3090,6 +3090,25 @@ fn resolves_parameter_ranges_with_enum_constants() { 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 preserves_enum_dependent_parameters_in_instance_overrides() { let source = r#" From cbf5e3ea62b4be37b98ecda3ef75c3d0157454b0 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 06:19:28 +0900 Subject: [PATCH 41/55] fix(sv-frontend): address follow-up review findings --- crates/celox-sv-analyzer/src/ast.rs | 226 +++++++++++++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 65 ++++++++ 2 files changed, 273 insertions(+), 18 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index a2c478192..d9721a2e8 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8274,6 +8274,7 @@ fn comb_assignments_from_guarded( mut guarded: Vec, packed_dimensions: &PackedDimensions, ) -> 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); @@ -8369,6 +8370,44 @@ fn comb_assignments_from_guarded( Ok(slots.into_iter().flatten().collect()) } +const MAX_DYNAMIC_SELECT_EXPANSION: u128 = 4_096; + +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(()) +} + fn comb_assignment_target_groups( guarded: &[ConditionalAssignment], const_env: &HashMap, @@ -9543,6 +9582,10 @@ fn dynamic_selected_value_after_write( 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 { @@ -11135,6 +11178,18 @@ fn two_state_case_item_reachability( } let (duplicate_reachability, has_duplicate) = case_item_duplicate_reachability(&labels_by_item, const_env); + 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())); }; @@ -11176,8 +11231,9 @@ fn two_state_case_item_reachability( if width > 128 { return has_duplicate.then_some((duplicate_reachability, default_index.is_some())); } - let constant_selector_value = expr_to_const(selector.clone()) - .and_then(|selector| eval_ast_const_expr(&selector, const_env)); + 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( @@ -11269,6 +11325,45 @@ fn two_state_case_item_reachability( } } +fn constant_case_item_reachability( + selector: &ConstExpr, + labels_by_item: &[Option>], + default_index: Option, + const_env: &HashMap, +) -> 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; + } + } + let covered = if let Some(index) = matched.or(default_index) { + reachable[index] = true; + true + } else { + false + }; + Some((reachable, covered)) +} + fn finite_four_state_case_item_reachability( labels_by_item: &[Option>], default_index: Option, @@ -11429,6 +11524,106 @@ fn definitely_assigned_comb_targets_statement_or_null( } } +fn written_comb_targets_statement_or_null( + stmt: &sv_parser::StatementOrNull, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, +) -> Option> { + let sv_parser::StatementOrNull::Statement(stmt) = stmt else { + return Some(Vec::new()); + }; + written_comb_targets(stmt, syntax_tree, packed_dimensions) +} + +fn written_comb_targets( + stmt: &sv_parser::Statement, + syntax_tree: &SyntaxTree, + 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); + } + } + 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::StatementItem::NonblockingAssignment(assignment) => { + Some(vec![variable_lvalue_from_node( + &assignment.0.nodes.0, + syntax_tree, + packed_dimensions, + )?]) + } + 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 (_, 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; + }; + written_comb_targets_statement_or_null( + &loop_statement.nodes.2, + syntax_tree, + packed_dimensions, + ) + } + _ => None, + } +} + fn definitely_assigned_comb_targets( stmt: &sv_parser::Statement, syntax_tree: &SyntaxTree, @@ -11451,16 +11646,22 @@ fn definitely_assigned_comb_targets( let mut targets = Vec::new(); let mut guarded_targets: Vec<(Expr, Vec)> = Vec::new(); for stmt in &block.nodes.3 { + 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, syntax_tree, packed_dimensions, ); - guarded_targets.retain(|(condition, _)| { - !statement_targets - .iter() - .any(|target| expr_references_lvalue(condition, target)) - }); + if let Some(written_targets) = &written_targets { + guarded_targets.retain(|(condition, _)| { + !written_targets + .iter() + .any(|target| expr_references_lvalue(condition, target)) + }); + } else { + guarded_targets.clear(); + } for target in statement_targets { if !targets.contains(&target) { targets.push(target); @@ -11469,9 +11670,6 @@ fn definitely_assigned_comb_targets( let Some((condition, branch_targets)) = guarded_comb_targets(stmt, syntax_tree, packed_dimensions) else { - if !statement_or_null_is_blocking_assignment(stmt) { - guarded_targets.clear(); - } continue; }; guarded_targets.retain(|(prior_condition, _)| { @@ -11658,14 +11856,6 @@ fn guarded_comb_targets( Some((condition, targets)) } -fn statement_or_null_is_blocking_assignment(stmt: &sv_parser::StatementOrNull) -> bool { - matches!( - stmt, - sv_parser::StatementOrNull::Statement(stmt) - if matches!(stmt.nodes.2, sv_parser::StatementItem::BlockingAssignment(_)) - ) -} - fn two_state_conditions_are_complements( left: &Expr, right: &Expr, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 30f3e8cde..7ea1a7ada 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -369,6 +369,28 @@ mod tests { .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 substitutes_reads_of_selected_comb_targets() { let ir = analyze_source( @@ -1192,6 +1214,49 @@ mod tests { .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 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( From 1e618ef1574678c05ea3a607d55a877a383fd23d Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 06:37:56 +0900 Subject: [PATCH 42/55] fix(sv-frontend): tighten case and guard analysis --- crates/celox-sv-analyzer/src/ast.rs | 117 ++++++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 85 ++++++++++++++++ crates/celox-sv-analyzer/src/typecheck.rs | 32 ++++++ 3 files changed, 216 insertions(+), 18 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index d9721a2e8..6e095a8c4 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -5,7 +5,10 @@ //! Celox runtime IR. use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; -use std::ops::{Deref, DerefMut}; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; use sv_parser::{Locate, RefNode, SyntaxTree, unwrap_node}; @@ -371,6 +374,8 @@ impl Module { ), ) })); + 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| { @@ -4422,6 +4427,8 @@ struct PackedDimensions { const_env: HashMap, type_aliases: HashMap, function_return_types: HashMap, bool, bool)>, + functions: Arc>, + expression_signedness: Arc>, } impl PackedDimensions { @@ -4435,6 +4442,8 @@ impl PackedDimensions { const_env: const_env.clone(), type_aliases: type_aliases.clone(), function_return_types: HashMap::default(), + functions: Arc::default(), + expression_signedness: Arc::default(), } } } @@ -8654,11 +8663,33 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap Expr::Resize { - expr: Box::new(simplify_constant_mux_conditions(*expr, const_env)), - width, - signed, - }, + } => { + let expr = simplify_constant_mux_conditions(*expr, const_env); + if let Some(value) = + expr_to_const(expr.clone()).and_then(|expr| eval_ast_const_expr(&expr, const_env)) + { + Expr::Literal(format_typed_parameter_literal(value, width, signed)) + } else if let Some(expr) = expr_to_const(expr.clone()) + && let Some(literal) = typecheck::context_size_const_integral_literal( + &crate::ir::ConstExpr::from(expr), + const_env, + ¶meter_types_from_const_env(const_env) + .into_iter() + .map(|(name, r#type)| (name, (r#type.width, r#type.signed))) + .collect(), + width, + signed, + ) + { + Expr::Literal(typecheck::format_integral_literal_binary(&literal)) + } else { + Expr::Resize { + expr: Box::new(expr), + width, + signed, + } + } + } Expr::Unary { op, expr } => Expr::Unary { op, expr: Box::new(simplify_constant_mux_conditions(*expr, const_env)), @@ -11148,6 +11179,13 @@ fn two_state_case_item_reachability( 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 mut labels_by_item = Vec::new(); let mut default_index = None; @@ -11176,8 +11214,8 @@ fn two_state_case_item_reachability( } } } - let (duplicate_reachability, has_duplicate) = - case_item_duplicate_reachability(&labels_by_item, const_env); + 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() @@ -11213,6 +11251,14 @@ fn two_state_case_item_reachability( 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, @@ -11427,7 +11473,12 @@ fn finite_four_state_case_item_reachability( fn case_item_duplicate_reachability( labels_by_item: &[Option>], const_env: &HashMap, + 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; @@ -11438,7 +11489,29 @@ fn case_item_duplicate_reachability( }; for label in labels { let duplicate = prior_labels.iter().any(|prior| { - *prior == label + let normalized_equal = selector_type.is_some_and(|selector_type| { + let prior_expr: crate::ir::ConstExpr = (*prior).clone().into(); + let label_expr: crate::ir::ConstExpr = label.clone().into(); + let prior = typecheck::context_size_const_integral_literal( + &prior_expr, + const_env, + &constant_types, + selector_type.width, + selector_type.signed, + ); + let label = typecheck::context_size_const_integral_literal( + &label_expr, + const_env, + &constant_types, + selector_type.width, + selector_type.signed, + ); + 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()), @@ -11655,9 +11728,13 @@ fn definitely_assigned_comb_targets( ); if let Some(written_targets) = &written_targets { guarded_targets.retain(|(condition, _)| { - !written_targets - .iter() - .any(|target| expr_references_lvalue(condition, target)) + !written_targets.iter().any(|target| { + expr_references_overlapping_lvalue( + condition, + target, + &packed_dimensions.const_env, + ) + }) }); } else { guarded_targets.clear(); @@ -11673,9 +11750,13 @@ fn definitely_assigned_comb_targets( continue; }; guarded_targets.retain(|(prior_condition, _)| { - !branch_targets - .iter() - .any(|target| expr_references_lvalue(prior_condition, target)) + !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( @@ -11847,9 +11928,9 @@ fn guarded_comb_targets( packed_dimensions, ); if targets.is_empty() - || targets - .iter() - .any(|target| expr_references_lvalue(&condition, target)) + || targets.iter().any(|target| { + expr_references_overlapping_lvalue(&condition, target, &packed_dimensions.const_env) + }) { return None; } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 7ea1a7ada..077761df5 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -391,6 +391,38 @@ mod tests { .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") + ); + } + #[test] fn substitutes_reads_of_selected_comb_targets() { let ir = analyze_source( @@ -1192,6 +1224,22 @@ mod tests { 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"); } #[test] @@ -1231,6 +1279,43 @@ mod tests { .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 caps_dynamic_select_normalization_expansion() { let error = analyze_source( diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index 5f2053532..04f7ac4dc 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -156,6 +156,38 @@ pub fn eval_const_integral_literal_with_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 { + 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, From 7fa6231a10294ced7d4931696fccf61d179dc5dc Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 07:00:02 +0900 Subject: [PATCH 43/55] fix(sv-frontend): address remaining review paths --- crates/celox-sv-analyzer/src/ast.rs | 265 ++++++++++++++---- crates/celox-sv-analyzer/src/lib.rs | 69 ++++- crates/celox-sv-analyzer/src/typecheck.rs | 5 + .../systemverilog/review_regressions.rs | 22 ++ 4 files changed, 301 insertions(+), 60 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 6e095a8c4..1573db180 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -2866,37 +2866,17 @@ fn parameters_from_module_node( ) -> 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(), + let RefNode::ParameterPortList(parameter_port_list) = parameter_port_list else { + unreachable!(); + }; + parameters_from_parameter_port_list( + parameter_port_list, syntax_tree, &mut parameters, - false, base_const_env, type_aliases, parameter_overrides, )?; - 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, - base_const_env, - type_aliases, - parameter_overrides, - )?; - } - } - for local in local_parameters { - if let Some(parameter) = parameters - .iter_mut() - .find(|parameter| parameter.name == local.name) - { - parameter.is_local = true; - } - } } for item in module_non_port_items(node.clone()) { @@ -2932,6 +2912,82 @@ fn parameters_from_module_node( 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 parameters_from_parameter_port_declaration( + declaration: &sv_parser::ParameterPortDeclaration, + syntax_tree: &SyntaxTree, + 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(()); + } + parameters_from_ref_node( + RefNode::ParameterPortDeclaration(declaration), + syntax_tree, + parameters, + is_local, + base_const_env, + type_aliases, + parameter_overrides, + ) +} + fn signals_from_module_node( node: RefNode<'_>, syntax_tree: &SyntaxTree, @@ -3263,16 +3319,27 @@ fn type_aliases_from_module_node_with_env( add_type_aliases_from_parameter_port_list( parameter_port_list, syntax_tree, + const_env, &mut aliases, ); } for child in parameter_port_list { match child { RefNode::LocalParameterDeclaration(localparam) => { - add_type_aliases_from_localparam(localparam, syntax_tree, &mut aliases); + add_type_aliases_from_localparam( + localparam, + syntax_tree, + const_env, + &mut aliases, + ); } RefNode::ParameterDeclaration(parameter) => { - add_type_aliases_from_parameter(parameter, syntax_tree, &mut aliases); + add_type_aliases_from_parameter( + parameter, + syntax_tree, + const_env, + &mut aliases, + ); } _ => {} } @@ -3292,10 +3359,15 @@ fn type_aliases_from_module_node_with_env( )?; } sv_parser::PackageOrGenerateItemDeclaration::LocalParameterDeclaration(localparam) => { - add_type_aliases_from_localparam(&localparam.0, syntax_tree, &mut aliases); + 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, &mut aliases); + add_type_aliases_from_parameter(¶meter.0, syntax_tree, const_env, &mut aliases); } _ => {} } @@ -3304,7 +3376,7 @@ fn type_aliases_from_module_node_with_env( let RefNode::TypeAssignment(assignment) = child else { continue; }; - add_type_alias_from_type_assignment(assignment, syntax_tree, &mut aliases); + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, &mut aliases); } Ok(aliases) } @@ -3366,17 +3438,28 @@ fn add_type_alias_from_data_declaration( 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, aliases); + add_type_aliases_from_parameter_port_declaration( + declaration, + syntax_tree, + const_env, + 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); + add_type_aliases_from_parameter_port_declaration( + declaration, + syntax_tree, + const_env, + aliases, + ); } } sv_parser::ParameterPortList::Empty(_) => {} @@ -3386,18 +3469,19 @@ fn add_type_aliases_from_parameter_port_list( 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, aliases); + add_type_aliases_from_parameter(declaration, syntax_tree, const_env, aliases); } sv_parser::ParameterPortDeclaration::LocalParameterDeclaration(declaration) => { - add_type_aliases_from_localparam(declaration, syntax_tree, aliases); + add_type_aliases_from_localparam(declaration, syntax_tree, const_env, aliases); } sv_parser::ParameterPortDeclaration::TypeList(list) => { for assignment in list.nodes.1.nodes.0.contents() { - add_type_alias_from_type_assignment(assignment, syntax_tree, aliases); + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); } } sv_parser::ParameterPortDeclaration::ParamList(_) => {} @@ -3407,32 +3491,35 @@ fn add_type_aliases_from_parameter_port_declaration( 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, aliases); + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); } } 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, aliases); + add_type_alias_from_type_assignment(assignment, syntax_tree, const_env, aliases); } } 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 { @@ -3442,7 +3529,12 @@ fn add_type_alias_from_type_assignment( else { return; }; - let Some(r#type) = type_from_ref_node(RefNode::DataType(data_type), syntax_tree) else { + 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); @@ -7643,7 +7735,7 @@ fn expand_assignment_calls( apply_return_type: bool, ) -> Assignment { Assignment::new( - assignment.lhs, + expand_lvalue_calls(assignment.lhs, functions, expression_signedness), expand_expr_calls( assignment.rhs, functions, @@ -7654,6 +7746,42 @@ fn expand_assignment_calls( ) } +fn expand_lvalue_calls( + lvalue: LValue, + functions: &HashMap, + expression_signedness: &HashMap, +) -> LValue { + let expand_bound = |bound: ConstExpr| { + let original = bound.clone(); + expr_to_const(expand_expr_calls( + const_expr_to_expr(bound), + functions, + expression_signedness, + 0, + false, + )) + .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, + }, + } +} + fn expr_signedness( expr: &Expr, identifiers: &HashMap, @@ -8665,29 +8793,28 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap { let expr = simplify_constant_mux_conditions(*expr, const_env); - if let Some(value) = - expr_to_const(expr.clone()).and_then(|expr| eval_ast_const_expr(&expr, const_env)) - { - Expr::Literal(format_typed_parameter_literal(value, width, signed)) - } else if let Some(expr) = expr_to_const(expr.clone()) - && let Some(literal) = typecheck::context_size_const_integral_literal( - &crate::ir::ConstExpr::from(expr), + if let Some(constant) = expr_to_const(expr.clone()) { + let parameter_types = parameter_types_from_const_env(const_env) + .into_iter() + .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_from_const_env(const_env) - .into_iter() - .map(|(name, r#type)| (name, (r#type.width, r#type.signed))) - .collect(), - width, - signed, - ) - { - Expr::Literal(typecheck::format_integral_literal_binary(&literal)) - } else { - Expr::Resize { - expr: Box::new(expr), + ¶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)); + } + } + Expr::Resize { + expr: Box::new(expr), + width, + signed, } } Expr::Unary { op, expr } => Expr::Unary { @@ -11922,6 +12049,13 @@ fn guarded_comb_targets( } 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, @@ -12255,7 +12389,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_const(expand_expr_calls( + expr, + &packed_dimensions.functions, + &packed_dimensions.expression_signedness, + 0, + false, + )) + }) .collect::>>()?; if let Some(range) = &select.nodes.2 { let sv_parser::PartSelectRange::ConstantRange(range) = &range.nodes.1 else { diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 077761df5..8d319729d 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -421,6 +421,34 @@ mod tests { .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] @@ -1316,6 +1344,45 @@ mod tests { .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( @@ -1952,7 +2019,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 04f7ac4dc..be5ab3bc2 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -165,6 +165,11 @@ pub fn context_size_const_integral_literal( 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)) diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index a5d26b0b5..1ec9210a5 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -3109,6 +3109,28 @@ fn resolves_named_casts_in_function_return_ranges() { 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#" From aa3fe3157fa6518a4499534168d2c5de7b3392a2 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 07:16:57 +0900 Subject: [PATCH 44/55] fix(sv-frontend): preserve overlapping read semantics --- crates/celox-sv-analyzer/src/ast.rs | 105 ++++++++++++++-------------- crates/celox-sv-analyzer/src/lib.rs | 47 +++++++++++++ 2 files changed, 101 insertions(+), 51 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 1573db180..b3853d43d 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8973,10 +8973,13 @@ fn substitute_intermediate_comb_value_reads( // 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_lvalue(condition, target)) - || expr_references_lvalue(assignment.assignment().rhs(), target) + 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(), @@ -9047,10 +9050,13 @@ fn substitute_intermediate_comb_value_reads( ), ); } else if !is_target_write - && (guarded_assignment - .condition() - .is_some_and(|condition| expr_references_lvalue(condition, target)) - || expr_references_lvalue(guarded_assignment.assignment().rhs(), target)) + && (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(), @@ -10128,39 +10134,6 @@ fn expr_matches_lvalue(expr: &Expr, target: &LValue) -> bool { } } -fn expr_references_lvalue(expr: &Expr, target: &LValue) -> bool { - if expr_matches_lvalue(expr, target) { - return true; - } - match expr { - Expr::Ident(_) | Expr::Literal(_) => false, - Expr::Select { expr, msb, lsb, .. } => { - expr_references_lvalue(expr, target) - || expr_references_lvalue(&const_expr_to_expr(msb.clone()), target) - || expr_references_lvalue(&const_expr_to_expr(lsb.clone()), target) - } - Expr::Resize { expr, .. } | Expr::Unary { expr, .. } => { - expr_references_lvalue(expr, target) - } - Expr::Concat(parts) | Expr::RepeatConcat { parts, .. } => parts - .iter() - .any(|part| expr_references_lvalue(part, target)), - Expr::Binary { left, right, .. } => { - expr_references_lvalue(left, target) || expr_references_lvalue(right, target) - } - Expr::Mux { - condition, - then_expr, - else_expr, - } => { - expr_references_lvalue(condition, target) - || expr_references_lvalue(then_expr, target) - || expr_references_lvalue(else_expr, target) - } - Expr::Call { args, .. } => args.iter().any(|arg| expr_references_lvalue(arg, target)), - } -} - fn expr_references_overlapping_lvalue( expr: &Expr, target: &LValue, @@ -11617,21 +11590,17 @@ fn case_item_duplicate_reachability( for label in labels { let duplicate = prior_labels.iter().any(|prior| { let normalized_equal = selector_type.is_some_and(|selector_type| { - let prior_expr: crate::ir::ConstExpr = (*prior).clone().into(); - let label_expr: crate::ir::ConstExpr = label.clone().into(); - let prior = typecheck::context_size_const_integral_literal( - &prior_expr, + let prior = case_label_selector_pattern( + prior, const_env, &constant_types, - selector_type.width, - selector_type.signed, + selector_type, ); - let label = typecheck::context_size_const_integral_literal( - &label_expr, + let label = case_label_selector_pattern( + label, const_env, &constant_types, - selector_type.width, - selector_type.signed, + selector_type, ); prior.zip(label).is_some_and(|(prior, label)| { prior.value == label.value && prior.mask == label.mask @@ -11656,6 +11625,40 @@ fn case_item_duplicate_reachability( (reachable, has_duplicate) } +fn case_label_selector_pattern( + label: &ConstExpr, + const_env: &HashMap, + 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; + } + } + } + literal.signed = comparison_signed; + let resized = + resize_integral_literal_for_cast(literal, selector_type.width, selector_type.signed); + typecheck::parse_integral_literal(&resized) +} + fn mark_condition_context( assignments: &mut [ConditionalAssignment], start: usize, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 8d319729d..09a3f9bea 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -726,6 +726,32 @@ mod tests { .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] @@ -1268,6 +1294,27 @@ mod tests { 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] From 16a0a65bfb8b98ef46d3e592c118de48a47f7b6c Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 07:31:55 +0900 Subject: [PATCH 45/55] fix(sv-frontend): fold predicates before coverage --- crates/celox-sv-analyzer/src/ast.rs | 30 ++++++++++++++++++++++ crates/celox-sv-analyzer/src/lib.rs | 39 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index b3853d43d..8ca8163c8 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -8886,6 +8886,23 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap) -> Expr { + let Some(constant) = expr_to_const(expr.clone()) else { + 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(); + typecheck::eval_const_integral_literal_with_types( + &crate::ir::ConstExpr::from(constant), + const_env, + ¶meter_types, + ) + .map(|literal| Expr::Literal(typecheck::format_integral_literal_binary(&literal))) + .unwrap_or(expr) +} + fn expr_is_intrinsically_two_state(expr: &Expr, const_env: &HashMap) -> bool { match expr { Expr::Ident(name) => const_env.contains_key(name), @@ -11287,6 +11304,7 @@ fn two_state_case_item_reachability( 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()) { @@ -11922,6 +11940,18 @@ fn definitely_assigned_comb_targets( ) { 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| { diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 09a3f9bea..7de6598f3 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1317,6 +1317,45 @@ mod tests { ); } + #[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 recognizes_complete_four_state_cases() { analyze_source( From 31cdab9f28845a5e2651c810629daa624b762a27 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 07:51:18 +0900 Subject: [PATCH 46/55] fix(sv-frontend): close remaining coverage gaps --- crates/celox-sv-analyzer/src/ast.rs | 226 +++++++++++++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 74 +++++++++ 2 files changed, 276 insertions(+), 24 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 8ca8163c8..a064de0e2 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -935,11 +935,17 @@ fn size_function_expression_type( type_aliases: &HashMap, first_dimension_only: bool, ) -> Option { - let packed_dimensions = PackedDimensions { + let mut packed_dimensions = 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 { match &expression { @@ -959,13 +965,92 @@ fn size_function_expression_type( .strip_prefix(VARIABLE_SIGNED_PREFIX) .map(|name| (name.to_string(), *signed != 0)) })); - let signed = expr_signedness(&expression, &identifier_signedness, &HashMap::default())?; + let signed = expr_signedness_with_return_types( + &expression, + &identifier_signedness, + &HashMap::default(), + &packed_dimensions.function_return_types, + )?; Some(ExprType { width: width.max(1), signed, }) } +fn containing_function_return_types( + target: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + type_aliases: &HashMap, +) -> HashMap, bool, bool)> { + 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; + } + return module + .into_iter() + .filter_map(|child| { + let RefNode::FunctionDeclaration(declaration) = child else { + return None; + }; + function_declaration_return_metadata( + declaration, + syntax_tree, + const_env, + type_aliases, + ) + }) + .collect(); + } + HashMap::default() +} + +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, (Option, bool, bool))> { + 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, + ( + return_type.map(|r#type| r#type.width), + return_type.is_some_and(|r#type| r#type.signed), + 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; @@ -3397,18 +3482,15 @@ fn add_type_alias_from_data_declaration( else { return Ok(()); }; - let r#type = type_from_ref_node_with_env( - RefNode::DataType(&declaration.nodes.1), - syntax_tree, - const_env, - aliases, - ) - .or_else(|| { - let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 else { - return None; - }; + let r#type = if let sv_parser::DataType::Enum(r#enum) = &declaration.nodes.1 { if let Some(base) = &r#enum.nodes.1 { - type_alias_from_ref_node(RefNode::EnumBaseType(base), syntax_tree, aliases) + 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; @@ -3418,7 +3500,14 @@ fn add_type_alias_from_data_declaration( )); 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(()); }; @@ -8887,20 +8976,65 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap) -> Expr { - let Some(constant) = expr_to_const(expr.clone()) else { - 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(); - typecheck::eval_const_integral_literal_with_types( - &crate::ir::ConstExpr::from(constant), - const_env, - ¶meter_types, - ) - .map(|literal| Expr::Literal(typecheck::format_integral_literal_binary(&literal))) - .unwrap_or(expr) + 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) + } + } +} + +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 expr_is_intrinsically_two_state(expr: &Expr, const_env: &HashMap) -> bool { @@ -12109,6 +12243,14 @@ fn two_state_conditions_are_complements( 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, @@ -12151,6 +12293,42 @@ fn two_state_conditions_are_complements( is_complement(left, right) || is_complement(right, left) || are_inverse_equalities(left, right) } +fn normalized_two_state_boolean<'a>( + expr: &'a Expr, + packed_dimensions: &PackedDimensions, +) -> Option<(&'a Expr, bool)> { + if let Expr::Unary { + op: UnaryOp::LogicNot, + expr, + } = expr + { + let (expr, positive) = normalized_two_state_boolean(expr, packed_dimensions)?; + return Some((expr, !positive)); + } + 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)) +} + +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, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 7de6598f3..b4fff7502 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1356,6 +1356,80 @@ mod tests { .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)); + } + + #[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( From 98605cd2ca68d7ff29c04ad2854f2acc795be375 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 08:05:06 +0900 Subject: [PATCH 47/55] fix(sv-frontend): guard function type discovery --- crates/celox-sv-analyzer/src/ast.rs | 56 ++++++++++++++++++++++++++--- crates/celox-sv-analyzer/src/lib.rs | 18 ++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index a064de0e2..dbe1da06f 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -6,6 +6,7 @@ use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::{ + cell::RefCell, ops::{Deref, DerefMut}, sync::Arc, }; @@ -998,24 +999,69 @@ fn containing_function_return_types( if target_start < module_start || target_end > module_end { continue; } - return module + 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; }; - function_declaration_return_metadata( + 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, - ) - }) - .collect(); + ) { + result.insert(name, metadata); + } + } + ACTIVE_FUNCTION_RETURN_METADATA.with(|active| { + active.borrow_mut().insert(module_span, result.clone()); + }); + } + return result; } HashMap::default() } +thread_local! { + static ACTIVE_FUNCTION_RETURN_METADATA: + RefCell, bool, bool)>>> = + RefCell::new(HashMap::default()); +} + +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; diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index b4fff7502..605c9b51e 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1409,6 +1409,24 @@ mod tests { ) .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] From 5aa3fa5d8fb93784488574aecd3810dec1410916 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 08:27:59 +0900 Subject: [PATCH 48/55] fix(sv-frontend): address follow-up review findings --- crates/celox-sv-analyzer/src/ast.rs | 174 +++++++++++++++++++++++----- crates/celox-sv-analyzer/src/lib.rs | 99 ++++++++++++++++ 2 files changed, 247 insertions(+), 26 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index dbe1da06f..18dcafd67 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -368,11 +368,12 @@ impl Module { .extend(functions.iter().map(|(name, function)| { ( name.clone(), - ( - function.return_width, - function.return_signed, - function.return_is_2state, - ), + 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()); @@ -560,6 +561,9 @@ fn static_for_loop_iterations( 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 { @@ -601,6 +605,29 @@ 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, @@ -951,6 +978,10 @@ fn size_function_expression_type( let width = if first_dimension_only { 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 { @@ -983,7 +1014,7 @@ fn containing_function_return_types( syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, -) -> HashMap, bool, bool)> { +) -> HashMap { let Some((target_start, target_end)) = ref_node_source_span(target) else { return HashMap::default(); }; @@ -1046,7 +1077,7 @@ fn containing_function_return_types( thread_local! { static ACTIVE_FUNCTION_RETURN_METADATA: - RefCell, bool, bool)>>> = + RefCell>> = RefCell::new(HashMap::default()); } @@ -1080,7 +1111,7 @@ fn function_declaration_return_metadata( syntax_tree: &SyntaxTree, const_env: &HashMap, type_aliases: &HashMap, -) -> Option<(String, (Option, bool, bool))> { +) -> 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), @@ -1089,11 +1120,18 @@ fn function_declaration_return_metadata( let return_type = function_return_type(return_node, syntax_tree, const_env, type_aliases); Some(( name, - ( - return_type.map(|r#type| r#type.width), - return_type.is_some_and(|r#type| r#type.signed), - function_return_is_2state(return_node, syntax_tree, type_aliases), - ), + 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), + }, )) } @@ -2818,6 +2856,7 @@ struct Function { params: Vec, body: Expr, return_width: Option, + return_first_packed_dimension_width: Option, return_signed: bool, return_is_2state: bool, } @@ -4648,12 +4687,20 @@ struct VariableDimensions { type VariablePackedDimensions = HashMap; +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct FunctionReturnMetadata { + width: Option, + first_packed_dimension_width: Option, + signed: bool, + is_2state: bool, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] struct PackedDimensions { variables: VariablePackedDimensions, const_env: HashMap, type_aliases: HashMap, - function_return_types: HashMap, bool, bool)>, + function_return_types: HashMap, functions: Arc>, expression_signedness: Arc>, } @@ -5908,6 +5955,13 @@ fn function_from_declaration( )?; 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 { @@ -5915,6 +5969,7 @@ fn function_from_declaration( 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, }) @@ -5968,6 +6023,13 @@ fn function_from_declaration( )?; 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 { @@ -5975,6 +6037,7 @@ fn function_from_declaration( 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, }) @@ -6146,6 +6209,43 @@ fn function_return_type( } } +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 = match node { + sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { + sv_parser::DataTypeOrVoid::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::DataTypeOrVoid::Void(_) => None, + }, + sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { + let node = RefNode::ImplicitDataType(data_type); + type_from_ref_node_with_env(node.clone(), syntax_tree, const_env, type_aliases) + .or_else(|| type_alias_from_ref_node(node, syntax_tree, 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 tf_params( list: &sv_parser::TfPortList, syntax_tree: &SyntaxTree, @@ -6987,11 +7087,13 @@ fn comb_processes_from_module_common_item( ); } 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, - packed_dimensions, + &local_packed_dimensions, functions, expression_signedness, parameter_literals, @@ -7929,7 +8031,7 @@ fn expr_signedness_with_return_types( expr: &Expr, identifiers: &HashMap, functions: &HashMap, - function_return_types: &HashMap, bool, bool)>, + function_return_types: &HashMap, ) -> Option { match expr { Expr::Ident(name) => identifiers.get(name).copied(), @@ -8017,7 +8119,7 @@ fn expr_signedness_with_return_types( .or_else(|| { function_return_types .get(name) - .map(|(_, signed, _)| *signed) + .map(|metadata| metadata.signed) }), } } @@ -9619,7 +9721,7 @@ fn expr_static_width(expr: &Expr, packed_dimensions: &PackedDimensions) -> Optio Expr::Call { name, .. } => packed_dimensions .function_return_types .get(name) - .and_then(|(width, _, _)| *width), + .and_then(|metadata| metadata.width), } } @@ -11422,7 +11524,7 @@ fn expr_is_two_state(expr: &Expr, packed_dimensions: &PackedDimensions) -> bool Expr::Call { name, .. } => packed_dimensions .function_return_types .get(name) - .is_some_and(|(_, _, is_2state)| *is_2state), + .is_some_and(|metadata| metadata.is_2state), } } @@ -11542,7 +11644,7 @@ fn two_state_case_item_reachability( packed_dimensions .function_return_types .get(name) - .map(|(_, signed, _)| *signed) + .map(|metadata| metadata.signed) } else { expr_signedness(&selector, &identifiers, &HashMap::default()) }; @@ -12004,7 +12106,7 @@ fn written_comb_targets( Some(targets) } sv_parser::StatementItem::LoopStatement(loop_statement) => { - let (_, values) = static_for_loop_iterations( + let (name, values) = static_for_loop_iterations( loop_statement, syntax_tree, &packed_dimensions.const_env, @@ -12015,11 +12117,31 @@ fn written_comb_targets( let sv_parser::LoopStatement::For(loop_statement) = &**loop_statement else { return None; }; - written_comb_targets_statement_or_null( - &loop_statement.nodes.2, - syntax_tree, - packed_dimensions, - ) + 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, } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 605c9b51e..02daa5a68 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1429,6 +1429,105 @@ mod tests { 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 restricts_enum_alias_types_to_the_declared_base() { let ir = analyze_source( From 0bbd1032949e9da4db3e3963a4a5f0cd601e9194 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 08:48:58 +0900 Subject: [PATCH 49/55] fix(sv-frontend): preserve typed selector semantics --- crates/celox-sv-analyzer/src/ast.rs | 45 +++++++++++++- crates/celox-sv-analyzer/src/lib.rs | 52 ++++++++++++++++ crates/celox-sv-analyzer/src/typecheck.rs | 75 ++++++++++++++++++----- 3 files changed, 154 insertions(+), 18 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 18dcafd67..9541b4ac0 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -963,11 +963,17 @@ fn size_function_expression_type( type_aliases: &HashMap, first_dimension_only: bool, ) -> Option { - let mut packed_dimensions = PackedDimensions { + let mut packed_dimensions = containing_packed_dimensions( + RefNode::Expression(argument), + syntax_tree, + 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, @@ -1009,6 +1015,39 @@ fn size_function_expression_type( }) } +fn containing_packed_dimensions( + target: RefNode<'_>, + syntax_tree: &SyntaxTree, + const_env: &HashMap, + 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; + } + 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 containing_function_return_types( target: RefNode<'_>, syntax_tree: &SyntaxTree, @@ -7995,7 +8034,7 @@ fn expand_lvalue_calls( functions, expression_signedness, 0, - false, + true, )) .unwrap_or(original) }; diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 02daa5a68..473801f2c 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1528,6 +1528,58 @@ mod tests { .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 applies_function_return_types_in_procedural_lvalue_indices() { + analyze_source( + r#" + module Top(input logic [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"); + } + #[test] fn restricts_enum_alias_types_to_the_declared_base() { let ir = analyze_source( diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index be5ab3bc2..c5e80824c 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -522,8 +522,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); @@ -532,20 +540,16 @@ 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 { @@ -584,6 +588,18 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option 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, + } => match integral_literal_truth(&integral_literal_from_const_expr(condition)?) { + Some(true) => integral_literal_from_const_expr(then_expr), + Some(false) => integral_literal_from_const_expr(else_expr), + None => Some(merge_unknown_integral_literals( + integral_literal_from_const_expr(then_expr)?, + integral_literal_from_const_expr(else_expr)?, + )), + }, _ => None, } } @@ -1121,6 +1137,35 @@ mod literal_tests { assert_eq!(eval_const_expr(&ne, &HashMap::default()), Some(1)); } + #[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 { From b017024e1bc41c48fb97a1f692b5b83ca15c219f Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 2 Sep 2026 09:09:29 +0900 Subject: [PATCH 50/55] fix(sv-frontend): address recursive review findings --- crates/celox-sv-analyzer/src/ast.rs | 156 +++++++++++++++++- crates/celox-sv-analyzer/src/lib.rs | 57 ++++++- .../systemverilog/review_regressions.rs | 41 +++++ 3 files changed, 243 insertions(+), 11 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 9541b4ac0..161395352 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -550,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, @@ -572,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( @@ -654,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(()) } @@ -665,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) => { @@ -672,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) => { @@ -684,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) => { @@ -701,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) => { @@ -709,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!(); }; @@ -727,6 +780,7 @@ fn validate_static_for_loops_in_statement( &loop_statement.nodes.2, syntax_tree, &loop_env, + remaining_expansion, )?; } } @@ -8029,7 +8083,7 @@ fn expand_lvalue_calls( ) -> LValue { let expand_bound = |bound: ConstExpr| { let original = bound.clone(); - expr_to_const(expand_expr_calls( + expr_to_lvalue_const(expand_expr_calls( const_expr_to_expr(bound), functions, expression_signedness, @@ -11642,6 +11696,16 @@ fn two_state_case_item_reachability( 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)) .and_then(expr_to_const) }) .collect::>>()?; @@ -12813,12 +12877,12 @@ fn lvalue_from_select( syntax_tree, packed_dimensions, )?; - expr_to_const(expand_expr_calls( + expr_to_lvalue_const(expand_expr_calls( expr, &packed_dimensions.functions, &packed_dimensions.expression_signedness, 0, - false, + true, )) }) .collect::>>()?; @@ -14272,6 +14336,80 @@ fn expr_to_const(expr: Expr) -> Option { } } +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_with_env( expr: &sv_parser::ConstantParamExpression, syntax_tree: &SyntaxTree, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 473801f2c..4adbee1fe 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1563,9 +1563,9 @@ mod tests { #[test] fn applies_function_return_types_in_procedural_lvalue_indices() { - analyze_source( + let ir = analyze_source( r#" - module Top(input logic [1:0] index, input logic data, output logic [1:0] x); + module Top(input bit [1:0] index, input logic data, output logic [1:0] x); function automatic bit idx(); return index; endfunction @@ -1578,6 +1578,59 @@ mod tests { 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 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] diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 1ec9210a5..9efb8dc57 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7753,6 +7753,47 @@ fn sign_extends_function_calls_in_conditional_assignments() { 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()); +} + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] #[test] fn collapses_unknown_initializers_in_two_state_native_images() { From 42148db1a92c62451c1c3bfe4a3f8802d7d7f7a2 Mon Sep 17 00:00:00 2001 From: tignear Date: Sun, 6 Sep 2026 20:09:14 +0900 Subject: [PATCH 51/55] fix(sv-frontend): resolve casts in ranges and generates --- crates/celox-sv-analyzer/src/ast.rs | 180 +++++++++++++----- crates/celox-sv-analyzer/src/lib.rs | 41 ++++ .../systemverilog/review_regressions.rs | 122 ++++++++++++ 3 files changed, 298 insertions(+), 45 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 161395352..e8b18c6c6 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -1553,7 +1553,8 @@ fn reject_silently_ignored_constructs( const_env: &HashMap, type_aliases: &HashMap, ) -> Result<(), AnalyzerError> { - let inactive_nodes = inactive_conditional_generate_nodes(node.clone(), syntax_tree, const_env); + 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)?; @@ -2000,10 +2001,17 @@ 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, &mut selections); + generate_selections_from_non_port_item( + item, + syntax_tree, + const_env, + type_aliases, + &mut selections, + ); } selections .into_iter() @@ -2031,12 +2039,19 @@ 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, selections); + generate_selections_from_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); } } sv_parser::NonPortModuleItem::ModuleOrGenerateItem(item) => { @@ -2044,6 +2059,7 @@ fn generate_selections_from_non_port_item<'a>( item, syntax_tree, const_env, + type_aliases, selections, ); } @@ -2055,10 +2071,17 @@ 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, selections); + generate_selections_from_module_or_generate_item( + item, + syntax_tree, + const_env, + type_aliases, + selections, + ); } } @@ -2066,6 +2089,7 @@ 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 { @@ -2077,11 +2101,18 @@ fn generate_selections_from_module_or_generate_item<'a>( generate, syntax_tree, const_env, + type_aliases, selections, ); } sv_parser::ModuleCommonItem::LoopGenerateConstruct(generate) => { - generate_selections_from_loop_generate(generate, syntax_tree, const_env, selections); + generate_selections_from_loop_generate( + generate, + syntax_tree, + const_env, + type_aliases, + selections, + ); } _ => {} } @@ -2091,14 +2122,17 @@ 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( + 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); @@ -2108,6 +2142,7 @@ fn generate_selections_from_conditional_generate<'a>( &generate.nodes.2, syntax_tree, const_env, + type_aliases, selections, ); } @@ -2115,7 +2150,13 @@ fn generate_selections_from_conditional_generate<'a>( 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); + generate_selections_from_generate_block( + block, + syntax_tree, + const_env, + type_aliases, + selections, + ); } } } @@ -2124,6 +2165,7 @@ 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( @@ -2135,13 +2177,16 @@ fn generate_selections_from_loop_generate<'a>( &generate.nodes.2, syntax_tree, const_env, + type_aliases, selections, ); return; }; - let Some(init) = const_expr_from_ref_node( + 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); @@ -2149,19 +2194,23 @@ fn generate_selections_from_loop_generate<'a>( &generate.nodes.2, syntax_tree, const_env, + type_aliases, selections, ); return; }; - let Some(condition) = const_expr_from_ref_node( + 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; @@ -2178,6 +2227,7 @@ fn generate_selections_from_loop_generate<'a>( &generate.nodes.2, syntax_tree, &loop_env, + type_aliases, selections, ); return; @@ -2190,11 +2240,16 @@ fn generate_selections_from_loop_generate<'a>( &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) - else { + let Some(next) = next_genvar_value( + value, + &generate.nodes.1.nodes.1.4, + syntax_tree, + &loop_env, + type_aliases, + ) else { break; }; value = next; @@ -2206,11 +2261,18 @@ 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, selections); + 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(); @@ -2218,7 +2280,13 @@ fn generate_selections_from_generate_block<'a>( if add_localparams_from_generate_item(item, syntax_tree, &mut block_env) { continue; } - generate_selections_from_generate_item(item, syntax_tree, &block_env, selections); + generate_selections_from_generate_item( + item, + syntax_tree, + &block_env, + type_aliases, + selections, + ); } } } @@ -3383,9 +3451,11 @@ fn signals_from_module_common_item( let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { return Ok(()); }; - let condition = const_expr_from_ref_node( + 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()) @@ -4937,15 +5007,22 @@ fn function_packed_dimension_widths(ranges: &[PackedRange]) -> Vec, +fn function_param_packed_dimensions( + data_type: &sv_parser::DataTypeOrImplicit, syntax_tree: &SyntaxTree, + const_env: &HashMap, type_aliases: &HashMap, ) -> Vec { - if let Some(alias) = type_alias_from_ref_node(node.clone(), syntax_tree, type_aliases) { + if let Some(alias) = type_alias_from_data_type_or_implicit(data_type, syntax_tree, type_aliases) + { function_packed_dimension_widths(alias.packed_ranges()) } else { - function_packed_dimension_widths(&packed_ranges_from_ref_node(node, syntax_tree)) + function_packed_dimension_widths(&packed_ranges_from_ref_node_with_env( + RefNode::DataTypeOrImplicit(data_type), + syntax_tree, + const_env, + type_aliases, + )) } } @@ -5313,9 +5390,11 @@ fn instances_from_conditional_generate( 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(), @@ -5588,8 +5667,10 @@ fn functions_from_module_node( 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); + 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; @@ -6361,12 +6442,12 @@ fn tf_params( .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); + 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() - && packed_ranges_from_ref_node(type_node.clone(), syntax_tree).is_empty(); + && 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) @@ -6461,9 +6542,10 @@ fn tf_item_params( ) }) .is_some_and(|r#type| r#type.kind() == TypeKind::Bit); - let packed_dimensions = packed_dimensions_from_ref_node( - RefNode::DataTypeOrImplicit(&declaration.nodes.3), + 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() { @@ -7244,9 +7326,11 @@ fn comb_processes_from_conditional_generate( 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(), @@ -7335,16 +7419,20 @@ fn comb_processes_from_loop_generate( syntax_tree, ) .ok_or_else(|| AnalyzerError::Unsupported("loop-generate variable".to_string()))?; - let init_expr = const_expr_from_ref_node( + 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( + 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()))?; @@ -7370,8 +7458,14 @@ fn comb_processes_from_loop_generate( 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()))?; + 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("genvar update operator".to_string()))?; value = next; } let mut loop_env = const_env.clone(); @@ -7616,10 +7710,11 @@ fn next_genvar_value( iteration: &sv_parser::GenvarIteration, syntax_tree: &SyntaxTree, const_env: &HashMap, + type_aliases: &HashMap, ) -> Option { match iteration { sv_parser::GenvarIteration::Prefix(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.0.nodes.0)?; + let op = syntax_tree.get_str(&iteration.nodes.0.nodes.0.nodes.0)?; match op { "++" => value.checked_add(1), "--" => value.checked_sub(1), @@ -7627,7 +7722,7 @@ fn next_genvar_value( } } sv_parser::GenvarIteration::Suffix(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0)?; + let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0.nodes.0)?; match op { "++" => value.checked_add(1), "--" => value.checked_sub(1), @@ -7635,10 +7730,12 @@ fn next_genvar_value( } } sv_parser::GenvarIteration::Assignment(iteration) => { - let op = syntax_tree.get_str(&iteration.nodes.1.nodes.0)?; - let rhs = const_expr_from_ref_node( + 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 { @@ -10805,9 +10902,11 @@ fn ff_processes_from_module_common_item( let sv_parser::ConditionalGenerateConstruct::If(generate) = &**generate else { return Ok(()); }; - let Some(condition) = const_expr_from_ref_node( + let Some(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( "unknown conditional-generate condition".to_string(), @@ -14644,15 +14743,6 @@ fn is_signed_from_ref_node(node: RefNode<'_>) -> Option { } } -fn packed_ranges_from_ref_node(node: RefNode<'_>, syntax_tree: &SyntaxTree) -> Vec { - packed_ranges_from_ref_node_with_env( - node, - syntax_tree, - &HashMap::default(), - &HashMap::default(), - ) -} - fn packed_ranges_from_ref_node_with_env( node: RefNode<'_>, syntax_tree: &SyntaxTree, diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 4adbee1fe..966eee08b 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1613,6 +1613,47 @@ mod tests { .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( diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 9efb8dc57..ee183aff5 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7794,6 +7794,128 @@ fn coerces_function_returns_in_procedural_lvalue_indices() { assert_eq!(sim.get(x), 2u8.into()); } +sv_backends! { + 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() { From aeefb6e368c1b52c855d4a68b52e3f221c3b8710 Mon Sep 17 00:00:00 2001 From: tignear Date: Wed, 9 Sep 2026 22:27:05 +0900 Subject: [PATCH 52/55] fix(sv-frontend): preserve constant types and size metadata --- crates/celox-sv-analyzer/src/ast.rs | 207 ++++++++++++++++-- crates/celox-sv-analyzer/src/lib.rs | 142 ++++++++++++ crates/celox-sv-analyzer/src/typecheck.rs | 156 +++++++++++-- .../systemverilog/review_regressions.rs | 146 ++++++++++++ 4 files changed, 607 insertions(+), 44 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index e8b18c6c6..986cbd86d 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -1036,14 +1036,15 @@ fn size_function_expression_type( ); let expression = expr_from_expression_with_types(argument, syntax_tree, &packed_dimensions)?; let width = if first_dimension_only { - 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), - } + 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 { expr_static_width(&expression, &packed_dimensions) }?; @@ -1069,6 +1070,63 @@ fn size_function_expression_type( }) } +fn selected_expression_first_dimension_width( + argument: &sv_parser::Expression, + syntax_tree: &SyntaxTree, + packed_dimensions: &PackedDimensions, +) -> Option { + let sv_parser::Expression::Primary(primary) = argument else { + return None; + }; + 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, + )?; + 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, @@ -1088,6 +1146,14 @@ fn containing_packed_dimensions( if target_start < module_start || target_end > module_end { continue; } + let module_span = (module_start, module_end); + if !ACTIVE_PACKED_DIMENSIONS.with(|active| active.borrow_mut().insert(module_span)) { + return 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 = @@ -1169,11 +1235,25 @@ fn containing_function_return_types( } 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), } @@ -2277,7 +2357,12 @@ fn generate_selections_from_generate_block<'a>( 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) { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + type_aliases, + ) { continue; } generate_selections_from_generate_item( @@ -3491,7 +3576,12 @@ fn signals_from_generate_block( 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) { + 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)?; @@ -5452,7 +5542,12 @@ fn instances_from_generate_block( 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) { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + &packed_dimensions.type_aliases, + ) { continue; } instances_from_generate_item( @@ -7785,7 +7880,12 @@ fn comb_processes_from_generate_block( 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) { + if add_localparams_from_generate_item( + item, + syntax_tree, + &mut block_env, + &packed_dimensions.type_aliases, + ) { continue; } comb_processes_from_generate_item( @@ -7809,14 +7909,22 @@ fn add_localparams_from_generate_item( item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, const_env: &mut HashMap, + type_aliases: &HashMap, ) -> bool { - add_localparams_from_generate_item_with_literals(item, syntax_tree, const_env, None) + add_localparams_from_generate_item_with_literals( + item, + syntax_tree, + const_env, + type_aliases, + None, + ) } fn add_localparams_from_generate_item_with_literals( item: &sv_parser::GenerateItem, syntax_tree: &SyntaxTree, const_env: &mut HashMap, + type_aliases: &HashMap, mut parameter_literals: Option<&mut HashMap>, ) -> bool { let sv_parser::GenerateItem::ModuleOrGenerateItem(item) = item else { @@ -7846,7 +7954,7 @@ fn add_localparams_from_generate_item_with_literals( &mut parameters, true, const_env, - &HashMap::default(), + type_aliases, &HashMap::default(), ) .is_err() @@ -9261,6 +9369,31 @@ fn simplify_constant_mux_conditions(expr: Expr, const_env: &HashMap else_expr, - Some(_) => then_expr, + 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), @@ -10965,6 +11130,7 @@ fn ff_processes_from_generate_block( item, syntax_tree, &mut block_env, + &packed_dimensions.type_aliases, Some(&mut block_parameter_literals), ) { continue; @@ -12667,10 +12833,11 @@ fn normalized_two_state_boolean<'a>( expr: &'a Expr, packed_dimensions: &PackedDimensions, ) -> Option<(&'a Expr, bool)> { - if let Expr::Unary { - op: UnaryOp::LogicNot, - expr, - } = expr + 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)); diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 966eee08b..be117955d 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1561,6 +1561,148 @@ mod tests { 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_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( diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index c5e80824c..8fd974eb5 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -433,23 +433,8 @@ fn eval_four_state_binary( 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), - _ => None, - }; - } - if !matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) { - return None; - } - let result = eval_four_state_binary_literal(left, op, right, signed)?; - integral_literal_as_i128(&result, signed) + integral_literal_as_i128(&result, result.signed) } fn eval_four_state_binary_literal( @@ -458,6 +443,18 @@ fn eval_four_state_binary_literal( right: &IntegralLiteral, signed: bool, ) -> Option { + if matches!(op, BinaryOp::LogicAnd | BinaryOp::LogicOr) { + let left = integral_literal_truth(left); + let right = integral_literal_truth(right); + 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) { return None; } @@ -490,6 +487,15 @@ fn eval_four_state_binary_literal( }) } +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 { let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); let known = width_mask ^ &literal.mask; @@ -555,6 +561,12 @@ fn merge_unknown_integral_literals( fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option { match expr { ConstExpr::Literal(literal) => parse_integral_literal(literal), + ConstExpr::Unary { + op: UnaryOp::LogicNot, + expr, + } => Some(integral_literal_from_truth( + integral_literal_truth(&integral_literal_from_const_expr(expr)?).map(|truth| !truth), + )), ConstExpr::Unary { op: UnaryOp::BitNot, expr, @@ -575,6 +587,15 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option literal.mask = BigUint::default(); Some(literal) } + 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::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) => { @@ -592,14 +613,36 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option condition, then_expr, else_expr, - } => match integral_literal_truth(&integral_literal_from_const_expr(condition)?) { - Some(true) => integral_literal_from_const_expr(then_expr), - Some(false) => integral_literal_from_const_expr(else_expr), - None => Some(merge_unknown_integral_literals( - integral_literal_from_const_expr(then_expr)?, - 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 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 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, } } @@ -1137,6 +1180,71 @@ 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 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 { diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index ee183aff5..3e494a579 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7794,7 +7794,153 @@ fn coerces_function_returns_in_procedural_lvalue_indices() { 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_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#" From 3f5038208b5cf05ce15914052c94000896953530 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 11 Sep 2026 03:32:16 +0900 Subject: [PATCH 53/55] fix(sv-frontend): fold four-state equality case selectors --- crates/celox-sv-analyzer/src/lib.rs | 36 ++++++ crates/celox-sv-analyzer/src/typecheck.rs | 115 ++++++++++++++++-- .../systemverilog/review_regressions.rs | 35 ++++++ 3 files changed, 179 insertions(+), 7 deletions(-) diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index be117955d..f953f894e 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1578,6 +1578,42 @@ mod tests { } } + #[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_selected_size_argument_dimensions() { for (argument, expected) in [ diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index 8fd974eb5..a77383d1b 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -256,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)?, @@ -277,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; @@ -455,7 +462,10 @@ fn eval_four_state_binary_literal( }; return Some(integral_literal_from_truth(truth)); } - if !matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) { + if !matches!( + op, + BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Eq | BinaryOp::Ne + ) { return None; } @@ -463,6 +473,21 @@ fn eval_four_state_binary_literal( 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; @@ -597,10 +622,12 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option eval_four_state_binary_literal(&left, *op, &right, false) } ConstExpr::Binary { left, op, right } - if matches!(op, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor) => + if matches!( + op, + BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Eq | BinaryOp::Ne + ) => { - let mut left = integral_literal_from_const_expr(left)?; - let mut right = integral_literal_from_const_expr(right)?; + 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); @@ -1220,6 +1247,80 @@ mod literal_tests { 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 context_sizes_known_constant_mux_arms() { for (condition, then_arm, else_arm, expected) in [ diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 3e494a579..9e883da03 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7808,6 +7808,41 @@ fn rejects_indexed_part_selects_in_comb_write_groups() { } sv_backends! { + 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#" From a92da132679031fa98c4990d84e751b87566444c Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 11 Sep 2026 04:12:11 +0900 Subject: [PATCH 54/55] fix(sv-frontend): preserve aliased function types and case constants --- crates/celox-sv-analyzer/src/ast.rs | 167 ++++++++---------- crates/celox-sv-analyzer/src/lib.rs | 76 ++++++++ crates/celox-sv-analyzer/src/typecheck.rs | 150 +++++++++++++--- .../systemverilog/review_regressions.rs | 149 ++++++++++++++++ 4 files changed, 429 insertions(+), 113 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index 986cbd86d..fca410e69 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -5103,17 +5103,14 @@ fn function_param_packed_dimensions( const_env: &HashMap, type_aliases: &HashMap, ) -> Vec { - if let Some(alias) = type_alias_from_data_type_or_implicit(data_type, syntax_tree, type_aliases) - { - function_packed_dimension_widths(alias.packed_ranges()) - } else { - function_packed_dimension_widths(&packed_ranges_from_ref_node_with_env( - RefNode::DataTypeOrImplicit(data_type), - syntax_tree, - const_env, - type_aliases, - )) - } + 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() } fn parameter_marker(name: &str) -> String { @@ -6485,23 +6482,12 @@ fn function_return_first_packed_dimension_width( type_aliases: &HashMap, return_type: Option, ) -> Option { - let r#type = match node { - sv_parser::FunctionDataTypeOrImplicit::DataTypeOrVoid(data_type) => match &**data_type { - sv_parser::DataTypeOrVoid::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::DataTypeOrVoid::Void(_) => None, - }, - sv_parser::FunctionDataTypeOrImplicit::ImplicitDataType(data_type) => { - let node = RefNode::ImplicitDataType(data_type); - type_from_ref_node_with_env(node.clone(), syntax_tree, const_env, type_aliases) - .or_else(|| type_alias_from_ref_node(node, syntax_tree, type_aliases)) - } - }; + 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()) @@ -6666,17 +6652,12 @@ fn value_type_from_data_type_or_implicit( 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, - ), - } + value_type_from_ref_node( + RefNode::DataTypeOrImplicit(node), + syntax_tree, + const_env, + type_aliases, + ) } fn value_type_from_data_type( @@ -6685,29 +6666,12 @@ fn value_type_from_data_type( const_env: &HashMap, type_aliases: &HashMap, ) -> Option { - let r#type = type_from_ref_node_with_env( + value_type_from_ref_node( RefNode::DataType(node), syntax_tree, const_env, type_aliases, ) - .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 value_type_from_ref_node( @@ -6716,40 +6680,54 @@ fn value_type_from_ref_node( 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_with_env( - node.clone(), - syntax_tree, - const_env, - type_aliases, - ); - &direct_ranges + let r#type = function_type_from_ref_node(node, syntax_tree, const_env, type_aliases)?; + expr_type_from_type(&r#type, const_env) +} + +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, }; - 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) - })? + // 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(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)), - }) + Some(type_with_fallback_ranges_with_env( + r#type, + node, + syntax_tree, + const_env, + type_aliases, + )) } fn integer_atom_expr_type(node: RefNode<'_>) -> Option { @@ -9479,6 +9457,11 @@ fn simplify_constant_mux_conditions(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))) @@ -11971,6 +11954,7 @@ fn two_state_case_item_reachability( ) }) .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::>>()?; @@ -14883,7 +14867,10 @@ fn type_with_fallback_ranges_with_env( 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; } diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index f953f894e..17cecc00e 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1614,6 +1614,82 @@ mod tests { } } + #[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_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 [ diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index a77383d1b..bdc765096 100644 --- a/crates/celox-sv-analyzer/src/typecheck.rs +++ b/crates/celox-sv-analyzer/src/typecheck.rs @@ -349,8 +349,17 @@ 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() + if matches!( + op, + BinaryOp::LogicAnd + | BinaryOp::LogicOr + | BinaryOp::Eq + | BinaryOp::Ne + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + ) || left.mask != BigUint::default() || right.mask != BigUint::default() { if let Some(result) = eval_four_state_binary(&left, op, &right, signed) { @@ -360,26 +369,6 @@ fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Opt return None; } } - if matches!( - op, - BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge - ) { - 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) { @@ -462,6 +451,30 @@ fn eval_four_state_binary_literal( }; return Some(integral_literal_from_truth(truth)); } + 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 @@ -624,7 +637,15 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option ConstExpr::Binary { left, op, right } if matches!( op, - BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Eq | BinaryOp::Ne + 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)?; @@ -1321,6 +1342,89 @@ mod literal_tests { } } + #[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 [ diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 9e883da03..35ea7b597 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7808,6 +7808,155 @@ fn rejects_indexed_part_selects_in_comb_write_groups() { } sv_backends! { + 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#" From d572df853d5fab453a6624ab867ce0a413054970 Mon Sep 17 00:00:00 2001 From: tignear Date: Fri, 11 Sep 2026 04:44:17 +0900 Subject: [PATCH 55/55] fix(sv-frontend): preserve parameter dimensions and constant masks --- crates/celox-sv-analyzer/src/ast.rs | 29 +- crates/celox-sv-analyzer/src/lib.rs | 126 +++++ crates/celox-sv-analyzer/src/typecheck.rs | 463 ++++++++++++------ .../systemverilog/review_regressions.rs | 99 ++++ 4 files changed, 557 insertions(+), 160 deletions(-) diff --git a/crates/celox-sv-analyzer/src/ast.rs b/crates/celox-sv-analyzer/src/ast.rs index fca410e69..9929dc4c5 100644 --- a/crates/celox-sv-analyzer/src/ast.rs +++ b/crates/celox-sv-analyzer/src/ast.rs @@ -4034,6 +4034,17 @@ fn type_alias_from_ref_node( 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); + } + RefNode::DataTypeOrImplicit(data_type) => { + return type_alias_from_data_type_or_implicit(data_type, syntax_tree, type_aliases); + } + _ => {} + } 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) { @@ -4214,17 +4225,13 @@ fn parameter_declared_width( } } env.extend(const_env_from_parameters(parameters)); - let ranges = declared_alias - .as_ref() - .map(|r#type| r#type.packed_ranges.clone()) - .unwrap_or_else(|| { - packed_ranges_from_ref_node_with_env( - node.clone(), - syntax_tree, - &range_env, - type_aliases, - ) - }); + 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()); + } if ranges.is_empty() { if declared_alias.is_some() { return Some(1); diff --git a/crates/celox-sv-analyzer/src/lib.rs b/crates/celox-sv-analyzer/src/lib.rs index 17cecc00e..41b71aff1 100644 --- a/crates/celox-sv-analyzer/src/lib.rs +++ b/crates/celox-sv-analyzer/src/lib.rs @@ -1668,6 +1668,132 @@ mod tests { 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( diff --git a/crates/celox-sv-analyzer/src/typecheck.rs b/crates/celox-sv-analyzer/src/typecheck.rs index bdc765096..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(), @@ -349,78 +349,7 @@ fn eval_literal_binary(left: &ConstExpr, op: BinaryOp, right: &ConstExpr) -> Opt signed, signed_extension(&right, signed), ); - if matches!( - op, - BinaryOp::LogicAnd - | BinaryOp::LogicOr - | BinaryOp::Eq - | BinaryOp::Ne - | BinaryOp::Lt - | BinaryOp::Le - | BinaryOp::Gt - | BinaryOp::Ge - ) || 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; - } - } - 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 value = match op { - BinaryOp::Div => left.value / right.value, - BinaryOp::Mod => left.value % right.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 { - width, - signed, - value, - mask: BigUint::default(), - }, - signed, - ) + eval_four_state_binary(&left, op, &right, signed) } fn eval_four_state_binary( @@ -439,6 +368,64 @@ fn eval_four_state_binary_literal( right: &IntegralLiteral, signed: bool, ) -> Option { + if matches!( + op, + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod + ) { + 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::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 Some(IntegralLiteral { + width, + signed, + value, + mask: BigUint::default(), + }); + } if matches!(op, BinaryOp::LogicAnd | BinaryOp::LogicOr) { let left = integral_literal_truth(left); let right = integral_literal_truth(right); @@ -599,31 +586,17 @@ fn merge_unknown_integral_literals( fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option { match expr { ConstExpr::Literal(literal) => parse_integral_literal(literal), - ConstExpr::Unary { - op: UnaryOp::LogicNot, - expr, - } => Some(integral_literal_from_truth( - integral_literal_truth(&integral_literal_from_const_expr(expr)?).map(|truth| !truth), - )), - ConstExpr::Unary { - op: UnaryOp::BitNot, - expr, - } => { - let mut literal = integral_literal_from_const_expr(expr)?; - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - let known = &width_mask ^ &literal.mask; - literal.value = ((&width_mask ^ literal.value) & known) | &literal.mask; - Some(literal) - } - ConstExpr::Unary { - op: UnaryOp::ToTwoState, - expr, - } => { - let mut literal = integral_literal_from_const_expr(expr)?; - let width_mask = (BigUint::from(1u8) << literal.width) - BigUint::from(1u8); - literal.value &= width_mask ^ &literal.mask; - literal.mask = BigUint::default(); - Some(literal) + 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) => @@ -637,7 +610,12 @@ fn integral_literal_from_const_expr(expr: &ConstExpr) -> Option ConstExpr::Binary { left, op, right } if matches!( op, - BinaryOp::BitAnd + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Eq @@ -712,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( @@ -1148,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(); @@ -1195,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] diff --git a/crates/celox/tests/frontends/systemverilog/review_regressions.rs b/crates/celox/tests/frontends/systemverilog/review_regressions.rs index 35ea7b597..216419229 100644 --- a/crates/celox/tests/frontends/systemverilog/review_regressions.rs +++ b/crates/celox/tests/frontends/systemverilog/review_regressions.rs @@ -7808,6 +7808,105 @@ fn rejects_indexed_part_selects_in_comb_write_groups() { } 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#"