use crate::expr::ast::{
BasicEventAst, BinaryOpAst, CastTargetAst, EventExprAst, InsideItemAst, IntegralBase,
IntegralLiteral, LogicalExprAst, LogicalExprNode, SelectionKindAst, UnaryOpAst,
};
use crate::expr::diagnostic::{DiagnosticLayer, ExprDiagnostic, Span};
use crate::expr::host::{
ExprStorage, ExprType, ExprTypeKind, ExpressionHost, IntegerLikeKind, SignalHandle,
};
use crate::expr::parser::parse_logical_expr_with_offset;
#[derive(Debug, Clone, PartialEq)]
pub struct BoundEventExpr {
pub(crate) terms: Vec<BoundEventTerm>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct BoundEventTerm {
pub event: BoundEventKind,
pub iff: Option<BoundLogicalExpr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BoundEventKind {
AnyTracked,
Named(SignalHandle),
Posedge(SignalHandle),
Negedge(SignalHandle),
Edge(SignalHandle),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BoundLogicalExpr {
pub(crate) root: BoundLogicalNode,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct BoundLogicalNode {
pub ty: ExprType,
pub span: Span,
pub kind: BoundLogicalKind,
}
impl BoundLogicalNode {
pub(crate) fn span(&self) -> Span {
self.span
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum BoundLogicalKind {
SignalRef {
handle: SignalHandle,
},
IntegralLiteral {
value: BoundIntegralValue,
is_unsized: bool,
},
RealLiteral {
value: f64,
},
StringLiteral {
value: String,
},
EnumLabel {
value: BoundIntegralValue,
label: String,
},
Parenthesized {
expr: Box<BoundLogicalNode>,
},
Cast {
kind: BoundCastKind,
expr: Box<BoundLogicalNode>,
},
Selection {
base: Box<BoundLogicalNode>,
selection: BoundSelection,
},
Unary {
op: UnaryOpAst,
expr: Box<BoundLogicalNode>,
},
Binary {
op: BinaryOpAst,
left: Box<BoundLogicalNode>,
right: Box<BoundLogicalNode>,
},
Conditional {
condition: Box<BoundLogicalNode>,
when_true: Box<BoundLogicalNode>,
when_false: Box<BoundLogicalNode>,
},
Inside {
expr: Box<BoundLogicalNode>,
set: Vec<BoundInsideItem>,
},
Concatenation {
items: Vec<BoundLogicalNode>,
},
Replication {
count: usize,
expr: Box<BoundLogicalNode>,
},
Triggered {
handle: SignalHandle,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BoundCastKind {
Signed,
Unsigned,
Static,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum BoundSelection {
Bit {
index: Box<BoundLogicalNode>,
},
Part {
msb: i64,
lsb: i64,
},
IndexedUp {
base: Box<BoundLogicalNode>,
width: usize,
},
IndexedDown {
base: Box<BoundLogicalNode>,
width: usize,
},
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum BoundInsideItem {
Expr(BoundLogicalNode),
Range {
low: BoundLogicalNode,
high: BoundLogicalNode,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BoundIntegralValue {
pub bits: Vec<BoundBit>,
pub signed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BoundBit {
Zero,
One,
X,
Z,
}
pub fn bind_event_expr_ast(
ast: &EventExprAst,
host: &dyn ExpressionHost,
) -> Result<BoundEventExpr, ExprDiagnostic> {
let mut terms = Vec::with_capacity(ast.terms.len());
for term in &ast.terms {
let event = match &term.event {
BasicEventAst::AnyTracked { .. } => BoundEventKind::AnyTracked,
BasicEventAst::Named { name, span } => {
BoundEventKind::Named(resolve_event_signal(host, name, *span)?)
}
BasicEventAst::Posedge { name, span } => {
let handle = resolve_event_signal(host, name, *span)?;
let ty = host.signal_type(handle)?;
ensure_integral(&ty, *span, "posedge operand")?;
BoundEventKind::Posedge(handle)
}
BasicEventAst::Negedge { name, span } => {
let handle = resolve_event_signal(host, name, *span)?;
let ty = host.signal_type(handle)?;
ensure_integral(&ty, *span, "negedge operand")?;
BoundEventKind::Negedge(handle)
}
BasicEventAst::Edge { name, span } => {
let handle = resolve_event_signal(host, name, *span)?;
let ty = host.signal_type(handle)?;
ensure_integral(&ty, *span, "edge operand")?;
BoundEventKind::Edge(handle)
}
};
let iff = if let Some(iff) = &term.iff {
let logical_ast = parse_logical_expr_with_offset(iff.source.as_str(), iff.span.start)?;
let bound = bind_logical_expr_ast(&logical_ast, host)?;
ensure_boolean_context_type(&bound.root.ty, bound.root.span, "iff guard")?;
Some(bound)
} else {
None
};
terms.push(BoundEventTerm { event, iff });
}
Ok(BoundEventExpr { terms })
}
fn resolve_event_signal(
host: &dyn ExpressionHost,
name: &str,
span: Span,
) -> Result<SignalHandle, ExprDiagnostic> {
host.resolve_signal(name).map_err(|inner| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "EXPR-SEMANTIC-UNKNOWN-SIGNAL",
message: format!("unknown signal '{name}'"),
primary_span: span,
notes: if inner.message.is_empty() {
vec![]
} else {
vec![format!("host detail: {}", inner.message)]
},
})
}
pub fn bind_logical_expr_ast(
ast: &LogicalExprAst,
host: &dyn ExpressionHost,
) -> Result<BoundLogicalExpr, ExprDiagnostic> {
let root = bind_logical_node(&ast.root, host)?;
Ok(BoundLogicalExpr { root })
}
fn bind_logical_node(
node: &LogicalExprNode,
host: &dyn ExpressionHost,
) -> Result<BoundLogicalNode, ExprDiagnostic> {
match node {
LogicalExprNode::OperandRef { name, span } => bind_logical_operand_ref(name, *span, host),
LogicalExprNode::IntegralLiteral { literal, span } => {
let value = decode_integral_literal(literal)?;
let is_four_state = match literal.base {
IntegralBase::Binary | IntegralBase::Hex => true,
IntegralBase::Decimal => value
.bits
.iter()
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z)),
};
let ty = bit_vector_type(
value.bits.len() as u32,
is_four_state,
literal.signed,
value.bits.len() > 1,
);
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::IntegralLiteral {
value,
is_unsized: literal.width.is_none(),
},
})
}
LogicalExprNode::RealLiteral { literal, span } => {
let value = literal.text.parse::<f64>().map_err(|_| {
sema_diag(
"EXPR-SEMANTIC-REAL-LITERAL",
"invalid real literal",
literal.span,
&["real literals must parse as finite 64-bit floating-point values"],
)
})?;
if !value.is_finite() {
return Err(sema_diag(
"EXPR-SEMANTIC-REAL-LITERAL",
"real literal is outside the supported finite range",
literal.span,
&["real literals must parse as finite 64-bit floating-point values"],
));
}
Ok(BoundLogicalNode {
ty: real_type(),
span: *span,
kind: BoundLogicalKind::RealLiteral { value },
})
}
LogicalExprNode::StringLiteral { literal, span } => Ok(BoundLogicalNode {
ty: string_type(),
span: *span,
kind: BoundLogicalKind::StringLiteral {
value: literal.value.clone(),
},
}),
LogicalExprNode::EnumLabel {
operand,
operand_span,
label,
label_span,
span,
} => {
let handle = host
.resolve_signal(operand)
.map_err(|inner| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "EXPR-SEMANTIC-UNKNOWN-SIGNAL",
message: format!("unknown signal '{operand}'"),
primary_span: *operand_span,
notes: if inner.message.is_empty() {
vec![]
} else {
vec![format!("host detail: {}", inner.message)]
},
})?;
let ty = host.signal_type(handle)?;
ensure_enum_type(&ty, *operand_span, "enum label operand")?;
let bits = lookup_enum_label_bits(&ty, label.as_str(), *label_span)?;
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::EnumLabel {
value: BoundIntegralValue {
bits: bits_from_sample(bits.as_str()),
signed: false,
},
label: label.clone(),
},
})
}
LogicalExprNode::Parenthesized { expr, span } => {
let expr = bind_logical_node(expr, host)?;
Ok(BoundLogicalNode {
ty: expr.ty.clone(),
span: *span,
kind: BoundLogicalKind::Parenthesized {
expr: Box::new(expr),
},
})
}
LogicalExprNode::Cast { target, expr, span } => {
let expr = bind_logical_node(expr, host)?;
let (ty, kind) = cast_target_type(target, &expr.ty, host, *span)?;
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::Cast {
kind,
expr: Box::new(expr),
},
})
}
LogicalExprNode::Selection {
base,
selection,
span,
} => {
if let Some(bound) = try_bind_canonical_signal_selection(base, selection, *span, host)?
{
return Ok(bound);
}
let base = bind_logical_node(base, host)?;
ensure_integral(&base.ty, base.span, "selection base")?;
if base.ty.storage != ExprStorage::PackedVector {
return Err(sema_diag(
"EXPR-SEMANTIC-SELECTION-BASE",
"selection base must be a packed integral value",
base.span,
&["selection of scalar integer-like or enum-core values is invalid"],
));
}
let (selection, ty) = match selection {
SelectionKindAst::Bit { index } => {
let index = bind_logical_node(index, host)?;
ensure_integral(&index.ty, index.span, "bit-select index")?;
(
BoundSelection::Bit {
index: Box::new(index),
},
bit_vector_type(1, base.ty.is_four_state, false, false),
)
}
SelectionKindAst::Part { msb, lsb } => {
let msb = bind_logical_node(msb, host)?;
let lsb = bind_logical_node(lsb, host)?;
let msb_value = eval_const_i64(&msb, "part-select msb", msb.span)?;
let lsb_value = eval_const_i64(&lsb, "part-select lsb", lsb.span)?;
let width = part_select_width(msb_value, lsb_value, *span)?;
let is_four_state =
invalid_part_select_forces_four_state(&base.ty, msb_value, lsb_value);
(
BoundSelection::Part {
msb: msb_value,
lsb: lsb_value,
},
bit_vector_type(width as u32, is_four_state, false, width > 1),
)
}
SelectionKindAst::IndexedUp {
base: index_base,
width,
} => {
let index_base = bind_logical_node(index_base, host)?;
ensure_integral(&index_base.ty, index_base.span, "indexed part-select base")?;
let width = bind_logical_node(width, host)?;
let width_value =
eval_const_i64(&width, "indexed part-select width", width.span)?;
let width_value = usize::try_from(width_value).map_err(|_| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"indexed part-select width must be positive",
width.span,
&["width must be a positive constant integer expression"],
)
})?;
if width_value == 0 {
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"indexed part-select width must be positive",
width.span,
&["width must be a positive constant integer expression"],
));
}
let is_four_state = indexed_part_select_may_produce_x(
&base.ty,
&index_base,
width_value,
true,
)?;
(
BoundSelection::IndexedUp {
base: Box::new(index_base),
width: width_value,
},
bit_vector_type(width_value as u32, is_four_state, false, width_value > 1),
)
}
SelectionKindAst::IndexedDown {
base: index_base,
width,
} => {
let index_base = bind_logical_node(index_base, host)?;
ensure_integral(&index_base.ty, index_base.span, "indexed part-select base")?;
let width = bind_logical_node(width, host)?;
let width_value =
eval_const_i64(&width, "indexed part-select width", width.span)?;
let width_value = usize::try_from(width_value).map_err(|_| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"indexed part-select width must be positive",
width.span,
&["width must be a positive constant integer expression"],
)
})?;
if width_value == 0 {
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"indexed part-select width must be positive",
width.span,
&["width must be a positive constant integer expression"],
));
}
let is_four_state = indexed_part_select_may_produce_x(
&base.ty,
&index_base,
width_value,
false,
)?;
(
BoundSelection::IndexedDown {
base: Box::new(index_base),
width: width_value,
},
bit_vector_type(width_value as u32, is_four_state, false, width_value > 1),
)
}
};
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::Selection {
base: Box::new(base),
selection,
},
})
}
LogicalExprNode::Unary { op, expr, span } => {
let expr = bind_logical_node(expr, host)?;
let ty = match op {
UnaryOpAst::LogicalNot => {
ensure_boolean_context_type(&expr.ty, expr.span, "logical operand")?;
bool_result_type()
}
UnaryOpAst::ReduceAnd
| UnaryOpAst::ReduceNand
| UnaryOpAst::ReduceOr
| UnaryOpAst::ReduceNor
| UnaryOpAst::ReduceXor
| UnaryOpAst::ReduceXnor => {
ensure_integral(&expr.ty, expr.span, "reduction operand")?;
bool_result_type()
}
UnaryOpAst::BitNot => {
ensure_integral(&expr.ty, expr.span, "bitwise operand")?;
non_enum_integral_type(&expr.ty)
}
UnaryOpAst::Plus | UnaryOpAst::Minus => {
ensure_numeric(&expr.ty, expr.span, "unary operand")?;
if matches!(expr.ty.kind, ExprTypeKind::Real) {
real_type()
} else {
non_enum_integral_type(&expr.ty)
}
}
};
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::Unary {
op: *op,
expr: Box::new(expr),
},
})
}
LogicalExprNode::Binary {
op,
left,
right,
span,
} => {
let left = bind_logical_node(left, host)?;
let right = bind_logical_node(right, host)?;
let ty = binary_result_type(*op, &left.ty, left.span, &right.ty, right.span)?;
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::Binary {
op: *op,
left: Box::new(left),
right: Box::new(right),
},
})
}
LogicalExprNode::Conditional {
condition,
when_true,
when_false,
span,
} => {
let condition = bind_logical_node(condition, host)?;
ensure_boolean_context_type(&condition.ty, condition.span, "conditional condition")?;
let when_true = bind_logical_node(when_true, host)?;
let when_false = bind_logical_node(when_false, host)?;
let ty = conditional_result_type(
&when_true.ty,
when_true.span,
&when_false.ty,
when_false.span,
)?;
Ok(BoundLogicalNode {
ty,
span: *span,
kind: BoundLogicalKind::Conditional {
condition: Box::new(condition),
when_true: Box::new(when_true),
when_false: Box::new(when_false),
},
})
}
LogicalExprNode::Inside { expr, set, span } => {
let expr = bind_logical_node(expr, host)?;
ensure_integral(&expr.ty, expr.span, "inside lhs")?;
let mut bound_set = Vec::with_capacity(set.len());
for item in set {
match item {
InsideItemAst::Expr(value) => {
let value = bind_logical_node(value, host)?;
ensure_integral(&value.ty, value.span, "inside set item")?;
bound_set.push(BoundInsideItem::Expr(value));
}
InsideItemAst::Range { low, high, .. } => {
let low = bind_logical_node(low, host)?;
let high = bind_logical_node(high, host)?;
ensure_integral(&low.ty, low.span, "inside range low")?;
ensure_integral(&high.ty, high.span, "inside range high")?;
bound_set.push(BoundInsideItem::Range { low, high });
}
}
}
Ok(BoundLogicalNode {
ty: bool_result_type(),
span: *span,
kind: BoundLogicalKind::Inside {
expr: Box::new(expr),
set: bound_set,
},
})
}
LogicalExprNode::Concatenation { items, span } => {
let mut bound_items = Vec::with_capacity(items.len());
let mut width_sum = 0u32;
let mut is_four_state = false;
for item in items {
let bound = bind_logical_node(item, host)?;
ensure_integral(&bound.ty, bound.span, "concatenation item")?;
if matches!(
bound.kind,
BoundLogicalKind::IntegralLiteral {
is_unsized: true,
..
}
) {
return Err(sema_diag(
"EXPR-SEMANTIC-CONCAT-UNSIZED",
"unsized constants are not allowed in concatenation",
bound.span,
&["cast or size constants before concatenating"],
));
}
width_sum = width_sum.checked_add(bound.ty.width).ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"concatenation width exceeds supported range",
bound.span,
&["concatenation result width must fit in u32"],
)
})?;
is_four_state |= bound.ty.is_four_state;
bound_items.push(bound);
}
if width_sum == 0 {
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"concatenation width must be greater than zero",
*span,
&["concatenation cannot produce an empty value"],
));
}
Ok(BoundLogicalNode {
ty: bit_vector_type(width_sum, is_four_state, false, true),
span: *span,
kind: BoundLogicalKind::Concatenation { items: bound_items },
})
}
LogicalExprNode::Replication { count, expr, span } => {
let count = bind_logical_node(count, host)?;
let count_value = eval_const_i64(&count, "replication multiplier", count.span)?;
let count_value = usize::try_from(count_value).map_err(|_| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"replication multiplier must be non-negative",
count.span,
&["replication form is {N{expr}} and N must be >= 0"],
)
})?;
if count_value == 0 {
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"replication multiplier must be greater than zero",
count.span,
&["replication multiplier must be greater than zero"],
));
}
let expr = bind_logical_node(expr, host)?;
ensure_integral(&expr.ty, expr.span, "replication operand")?;
let width = expr
.ty
.width
.checked_mul(count_value as u32)
.ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"replication width exceeds supported range",
*span,
&["replication result width must fit in u32"],
)
})?;
Ok(BoundLogicalNode {
ty: bit_vector_type(width, expr.ty.is_four_state, false, true),
span: *span,
kind: BoundLogicalKind::Replication {
count: count_value,
expr: Box::new(expr),
},
})
}
LogicalExprNode::Triggered { expr, span } => match expr.as_ref() {
LogicalExprNode::OperandRef {
name,
span: operand_span,
} => {
let handle = host.resolve_signal(name).map_err(|inner| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "EXPR-SEMANTIC-UNKNOWN-SIGNAL",
message: format!("unknown signal '{name}'"),
primary_span: *operand_span,
notes: if inner.message.is_empty() {
vec![]
} else {
vec![format!("host detail: {}", inner.message)]
},
})?;
let ty = host.signal_type(handle)?;
if !matches!(&ty.kind, ExprTypeKind::Event) {
return Err(sema_diag(
"EXPR-SEMANTIC-TRIGGERED",
".triggered() requires a raw event operand",
*operand_span,
&["only operands with event type support .triggered()"],
));
}
Ok(BoundLogicalNode {
ty: bit_vector_type(1, false, false, false),
span: *span,
kind: BoundLogicalKind::Triggered { handle },
})
}
LogicalExprNode::Triggered { .. } => Err(sema_diag(
"EXPR-SEMANTIC-TRIGGERED",
"chained .triggered() is invalid",
*span,
&["apply .triggered() only once to a raw event operand reference"],
)),
other => {
if matches!(other, LogicalExprNode::Selection { .. }) {
return match bind_logical_node(other, host) {
Ok(_) => Err(sema_diag(
"EXPR-SEMANTIC-TRIGGERED",
".triggered() requires a raw event operand",
other.span(),
&["apply .triggered() directly to an event operand reference"],
)),
Err(err) => Err(err),
};
}
Err(sema_diag(
"EXPR-SEMANTIC-TRIGGERED",
".triggered() requires a raw event operand",
other.span(),
&["apply .triggered() directly to an event operand reference"],
))
}
},
}
}
fn bind_logical_operand_ref(
name: &str,
span: Span,
host: &dyn ExpressionHost,
) -> Result<BoundLogicalNode, ExprDiagnostic> {
let handle = host.resolve_signal(name).map_err(|inner| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "EXPR-SEMANTIC-UNKNOWN-SIGNAL",
message: format!("unknown signal '{name}'"),
primary_span: span,
notes: if inner.message.is_empty() {
vec![]
} else {
vec![format!("host detail: {}", inner.message)]
},
})?;
bind_resolved_logical_signal(handle, span, host)
}
fn bind_resolved_logical_signal(
handle: SignalHandle,
span: Span,
host: &dyn ExpressionHost,
) -> Result<BoundLogicalNode, ExprDiagnostic> {
let ty = host.signal_type(handle)?;
if matches!(&ty.kind, ExprTypeKind::Event) {
return Err(sema_diag(
"EXPR-SEMANTIC-EVENT-VALUE",
"raw event operands are only valid with .triggered()",
span,
&["use event_operand.triggered() to read a raw event occurrence"],
));
}
Ok(BoundLogicalNode {
ty,
span,
kind: BoundLogicalKind::SignalRef { handle },
})
}
fn try_bind_canonical_signal_selection(
base: &LogicalExprNode,
selection: &SelectionKindAst,
span: Span,
host: &dyn ExpressionHost,
) -> Result<Option<BoundLogicalNode>, ExprDiagnostic> {
let Some(name) = canonical_signal_name_from_selection(base, selection) else {
return Ok(None);
};
match host.resolve_signal(name.as_str()) {
Ok(handle) => bind_resolved_logical_signal(handle, span, host).map(Some),
Err(err) if err.code == "HOST-UNKNOWN-SIGNAL" => Ok(None),
Err(err) => Err(err),
}
}
fn canonical_signal_name_from_selection(
base: &LogicalExprNode,
selection: &SelectionKindAst,
) -> Option<String> {
let base_name = match base {
LogicalExprNode::OperandRef { name, .. } => name,
_ => return None,
};
let index = match selection {
SelectionKindAst::Bit { index } => match index.as_ref() {
LogicalExprNode::IntegralLiteral { literal, .. }
if literal.width.is_none()
&& literal.base == IntegralBase::Decimal
&& literal.digits.chars().all(|ch| ch.is_ascii_digit()) =>
{
literal.digits.as_str()
}
_ => return None,
},
_ => return None,
};
Some(format!("{base_name}[{index}]"))
}
fn ensure_integral(ty: &ExprType, span: Span, context: &str) -> Result<(), ExprDiagnostic> {
if is_integral_type(ty) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-INTEGRAL-REQUIRED",
"integral operand is required",
span,
&[context],
))
}
fn ensure_numeric(ty: &ExprType, span: Span, context: &str) -> Result<(), ExprDiagnostic> {
if is_integral_type(ty) || matches!(&ty.kind, ExprTypeKind::Real) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-NUMERIC",
"numeric operand is required",
span,
&[context],
))
}
fn ensure_boolean_context_type(
ty: &ExprType,
span: Span,
context: &str,
) -> Result<(), ExprDiagnostic> {
if is_boolean_context_type(ty) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-BOOLEAN-CONTEXT",
"logical operators require integral or real operands",
span,
&[context],
))
}
fn ensure_enum_type(ty: &ExprType, span: Span, context: &str) -> Result<(), ExprDiagnostic> {
if matches!(&ty.kind, ExprTypeKind::EnumCore) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-ENUM-LABEL",
"enum label references require an enum-typed operand",
span,
&[context],
))
}
fn is_integral_type(ty: &ExprType) -> bool {
matches!(
ty.kind,
ExprTypeKind::BitVector | ExprTypeKind::IntegerLike(_) | ExprTypeKind::EnumCore
)
}
fn is_boolean_context_type(ty: &ExprType) -> bool {
is_integral_type(ty) || matches!(&ty.kind, ExprTypeKind::Real)
}
fn bool_result_type() -> ExprType {
bit_vector_type(1, true, false, false)
}
fn real_type() -> ExprType {
ExprType {
kind: ExprTypeKind::Real,
storage: ExprStorage::Scalar,
width: 64,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
}
}
fn string_type() -> ExprType {
ExprType {
kind: ExprTypeKind::String,
storage: ExprStorage::Scalar,
width: 0,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
}
}
fn bit_vector_type(width: u32, is_four_state: bool, is_signed: bool, packed: bool) -> ExprType {
ExprType {
kind: ExprTypeKind::BitVector,
storage: if packed {
ExprStorage::PackedVector
} else {
ExprStorage::Scalar
},
width: width.max(1),
is_four_state,
is_signed,
enum_type_id: None,
enum_labels: None,
}
}
fn integer_like_type(kind: IntegerLikeKind) -> ExprType {
let (width, is_signed, is_four_state) = match kind {
IntegerLikeKind::Byte => (8, true, false),
IntegerLikeKind::Shortint => (16, true, false),
IntegerLikeKind::Int => (32, true, false),
IntegerLikeKind::Longint => (64, true, false),
IntegerLikeKind::Integer => (32, true, true),
IntegerLikeKind::Time => (64, false, true),
};
ExprType {
kind: ExprTypeKind::IntegerLike(kind),
storage: ExprStorage::Scalar,
width,
is_four_state,
is_signed,
enum_type_id: None,
enum_labels: None,
}
}
fn cast_target_type(
target: &CastTargetAst,
source: &ExprType,
host: &dyn ExpressionHost,
span: Span,
) -> Result<(ExprType, BoundCastKind), ExprDiagnostic> {
match target {
CastTargetAst::Signed => {
if !is_integral_type(source) {
return Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"signed cast requires an integral source",
span,
&["signed'(expr) is valid only for integral expr"],
));
}
let mut ty = non_enum_integral_type(source);
ty.is_signed = true;
Ok((ty, BoundCastKind::Signed))
}
CastTargetAst::Unsigned => {
if !is_integral_type(source) {
return Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"unsigned cast requires an integral source",
span,
&["unsigned'(expr) is valid only for integral expr"],
));
}
let mut ty = non_enum_integral_type(source);
ty.is_signed = false;
Ok((ty, BoundCastKind::Unsigned))
}
CastTargetAst::BitVector {
width,
is_four_state,
is_signed,
} => {
ensure_cast_compatible(source, &ExprTypeKind::BitVector, span)?;
Ok((
bit_vector_type(*width, *is_four_state, *is_signed, *width > 1),
BoundCastKind::Static,
))
}
CastTargetAst::IntegerLike(kind) => {
ensure_cast_compatible(source, &ExprTypeKind::IntegerLike(*kind), span)?;
Ok((integer_like_type(*kind), BoundCastKind::Static))
}
CastTargetAst::Real => {
ensure_real_cast_source(source, span)?;
Ok((real_type(), BoundCastKind::Static))
}
CastTargetAst::String => {
ensure_string_cast_source(source, span)?;
Ok((string_type(), BoundCastKind::Static))
}
CastTargetAst::RecoveredType {
name,
span: target_span,
} => {
let handle = host.resolve_signal(name).map_err(|inner| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "EXPR-SEMANTIC-UNKNOWN-SIGNAL",
message: format!("unknown signal '{name}'"),
primary_span: *target_span,
notes: if inner.message.is_empty() {
vec![]
} else {
vec![format!("host detail: {}", inner.message)]
},
})?;
let ty = host.signal_type(handle)?;
if matches!(&ty.kind, ExprTypeKind::Event) {
return Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"raw event operands cannot be used as cast targets",
*target_span,
&["type(event_operand_reference)'(...) is invalid"],
));
}
if matches!(&ty.kind, ExprTypeKind::EnumCore) && ty.enum_type_id.is_none() {
return Err(sema_diag(
"EXPR-SEMANTIC-METADATA",
"metadata for the recovered enum type is unavailable",
*target_span,
&["enum operand-type casts require recovered enum type metadata"],
));
}
match &ty.kind {
ExprTypeKind::Real => ensure_real_cast_source(source, span)?,
ExprTypeKind::String => ensure_string_cast_source(source, span)?,
ExprTypeKind::BitVector | ExprTypeKind::IntegerLike(_) | ExprTypeKind::EnumCore => {
ensure_cast_compatible(source, &ty.kind, span)?
}
ExprTypeKind::Event => unreachable!(),
}
Ok((ty, BoundCastKind::Static))
}
}
}
fn ensure_cast_compatible(
source: &ExprType,
target_kind: &ExprTypeKind,
span: Span,
) -> Result<(), ExprDiagnostic> {
match target_kind {
ExprTypeKind::BitVector | ExprTypeKind::IntegerLike(_) | ExprTypeKind::EnumCore => {
if is_integral_type(source) || matches!(&source.kind, ExprTypeKind::Real) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"integral cast target requires an integral or real source",
span,
&["string and raw event operands cannot be cast to integral targets"],
))
}
_ => Ok(()),
}
}
fn ensure_real_cast_source(source: &ExprType, span: Span) -> Result<(), ExprDiagnostic> {
if is_integral_type(source) || matches!(&source.kind, ExprTypeKind::Real) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"real cast requires an integral or real source",
span,
&["string and raw event operands cannot be cast to real"],
))
}
fn ensure_string_cast_source(source: &ExprType, span: Span) -> Result<(), ExprDiagnostic> {
if matches!(&source.kind, ExprTypeKind::String) {
return Ok(());
}
Err(sema_diag(
"EXPR-SEMANTIC-CAST-TARGET",
"string cast is supported only as string identity",
span,
&["string'(expr) is valid only when expr already has string type"],
))
}
fn non_enum_integral_type(ty: &ExprType) -> ExprType {
match &ty.kind {
ExprTypeKind::EnumCore => {
bit_vector_type(ty.width, ty.is_four_state, ty.is_signed, ty.width > 1)
}
_ => ty.clone(),
}
}
fn common_integral_type(left: &ExprType, right: &ExprType) -> ExprType {
let width = left.width.max(right.width).max(1);
let is_signed = left.is_signed && right.is_signed;
let is_four_state = left.is_four_state || right.is_four_state;
if let (ExprTypeKind::IntegerLike(lhs_kind), ExprTypeKind::IntegerLike(rhs_kind)) =
(&left.kind, &right.kind)
&& lhs_kind == rhs_kind
{
let expected = integer_like_type(*lhs_kind);
if expected.width == width
&& expected.is_signed == is_signed
&& expected.is_four_state == is_four_state
{
return expected;
}
}
bit_vector_type(width, is_four_state, is_signed, width > 1)
}
fn common_numeric_result_type(left: &ExprType, right: &ExprType) -> ExprType {
if matches!(&left.kind, ExprTypeKind::Real) || matches!(&right.kind, ExprTypeKind::Real) {
real_type()
} else {
common_integral_type(left, right)
}
}
fn conditional_result_type(
when_true: &ExprType,
when_true_span: Span,
when_false: &ExprType,
when_false_span: Span,
) -> Result<ExprType, ExprDiagnostic> {
if matches!(&when_true.kind, ExprTypeKind::EnumCore)
&& matches!(&when_false.kind, ExprTypeKind::EnumCore)
&& when_true.enum_type_id.is_some()
&& when_true.enum_type_id == when_false.enum_type_id
{
return Ok(when_true.clone());
}
if matches!(&when_true.kind, ExprTypeKind::String)
&& matches!(&when_false.kind, ExprTypeKind::String)
{
return Ok(string_type());
}
if is_integral_type(when_true) && is_integral_type(when_false) {
return Ok(common_integral_type(when_true, when_false));
}
if (is_integral_type(when_true) || matches!(&when_true.kind, ExprTypeKind::Real))
&& (is_integral_type(when_false) || matches!(&when_false.kind, ExprTypeKind::Real))
{
return Ok(common_numeric_result_type(when_true, when_false));
}
Err(sema_diag(
"EXPR-SEMANTIC-CONDITIONAL-TYPE",
"conditional result arms do not have a common type",
Span::new(when_true_span.start, when_false_span.end),
&["both result arms must be compatible integral, real, enum, or string values"],
))
}
fn binary_result_type(
op: BinaryOpAst,
left: &ExprType,
left_span: Span,
right: &ExprType,
right_span: Span,
) -> Result<ExprType, ExprDiagnostic> {
if is_integral_type(left) && is_integral_type(right) {
return Ok(match op {
BinaryOpAst::LogicalAnd
| BinaryOpAst::LogicalOr
| BinaryOpAst::Lt
| BinaryOpAst::Le
| BinaryOpAst::Gt
| BinaryOpAst::Ge
| BinaryOpAst::Eq
| BinaryOpAst::Ne
| BinaryOpAst::CaseEq
| BinaryOpAst::CaseNe
| BinaryOpAst::WildEq
| BinaryOpAst::WildNe => bool_result_type(),
BinaryOpAst::ShiftLeft
| BinaryOpAst::ShiftRight
| BinaryOpAst::ShiftArithLeft
| BinaryOpAst::ShiftArithRight => non_enum_integral_type(left),
BinaryOpAst::BitAnd
| BinaryOpAst::BitXor
| BinaryOpAst::BitXnor
| BinaryOpAst::BitOr
| BinaryOpAst::Power
| BinaryOpAst::Multiply
| BinaryOpAst::Divide
| BinaryOpAst::Modulo
| BinaryOpAst::Add
| BinaryOpAst::Subtract => common_integral_type(left, right),
});
}
match op {
BinaryOpAst::LogicalAnd | BinaryOpAst::LogicalOr => {
ensure_boolean_context_type(left, left_span, "binary lhs")?;
ensure_boolean_context_type(right, right_span, "binary rhs")?;
Ok(bool_result_type())
}
BinaryOpAst::Lt | BinaryOpAst::Le | BinaryOpAst::Gt | BinaryOpAst::Ge => {
ensure_numeric(left, left_span, "comparison lhs")?;
ensure_numeric(right, right_span, "comparison rhs")?;
Ok(bool_result_type())
}
BinaryOpAst::Eq | BinaryOpAst::Ne => {
if matches!(&left.kind, ExprTypeKind::String)
|| matches!(&right.kind, ExprTypeKind::String)
{
if matches!(&left.kind, ExprTypeKind::String)
&& matches!(&right.kind, ExprTypeKind::String)
{
return Ok(bool_result_type());
}
return Err(sema_diag(
"EXPR-SEMANTIC-EQUALITY-TYPE",
"string equality requires string operands on both sides",
Span::new(left_span.start, right_span.end),
&["string values do not use numeric coercion"],
));
}
ensure_numeric(left, left_span, "equality lhs")?;
ensure_numeric(right, right_span, "equality rhs")?;
Ok(bool_result_type())
}
BinaryOpAst::CaseEq | BinaryOpAst::CaseNe | BinaryOpAst::WildEq | BinaryOpAst::WildNe => {
ensure_integral(left, left_span, "integral equality lhs")?;
ensure_integral(right, right_span, "integral equality rhs")?;
Ok(bool_result_type())
}
BinaryOpAst::ShiftLeft
| BinaryOpAst::ShiftRight
| BinaryOpAst::ShiftArithLeft
| BinaryOpAst::ShiftArithRight => {
ensure_integral(left, left_span, "shift lhs")?;
ensure_integral(right, right_span, "shift rhs")?;
Ok(non_enum_integral_type(left))
}
BinaryOpAst::BitAnd | BinaryOpAst::BitXor | BinaryOpAst::BitXnor | BinaryOpAst::BitOr => {
ensure_integral(left, left_span, "bitwise lhs")?;
ensure_integral(right, right_span, "bitwise rhs")?;
Ok(common_integral_type(left, right))
}
BinaryOpAst::Power
| BinaryOpAst::Multiply
| BinaryOpAst::Divide
| BinaryOpAst::Modulo
| BinaryOpAst::Add
| BinaryOpAst::Subtract => {
ensure_numeric(left, left_span, "numeric lhs")?;
ensure_numeric(right, right_span, "numeric rhs")?;
Ok(common_numeric_result_type(left, right))
}
}
}
fn lookup_enum_label_bits(
ty: &ExprType,
label: &str,
span: Span,
) -> Result<String, ExprDiagnostic> {
let labels = ty.enum_labels.as_ref().ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-METADATA",
"metadata for enum labels is unavailable",
span,
&["enum label references require recovered enum label metadata"],
)
})?;
labels
.iter()
.find(|entry| entry.name == label)
.map(|entry| entry.bits.clone())
.ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-ENUM-LABEL",
"enum label does not exist in the recovered type",
span,
&["type(enum_operand_reference)::LABEL requires a declared label"],
)
})
}
fn decode_integral_literal(
literal: &IntegralLiteral,
) -> Result<BoundIntegralValue, ExprDiagnostic> {
let mut bits = match literal.base {
IntegralBase::Binary => literal
.digits
.chars()
.map(bit_from_char)
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
sema_diag(
"EXPR-PARSE-LOGICAL-LITERAL",
"invalid binary integral literal",
literal.span,
&["binary literals may use 0, 1, x, z"],
)
})?,
IntegralBase::Hex => {
let mut raw = Vec::new();
for ch in literal.digits.chars() {
push_hex_nibble(ch, &mut raw).ok_or_else(|| {
sema_diag(
"EXPR-PARSE-LOGICAL-LITERAL",
"invalid hexadecimal integral literal",
literal.span,
&["hex literals may use 0-9, a-f, x, z"],
)
})?;
}
raw
}
IntegralBase::Decimal => {
if literal.digits.chars().all(|ch| ch.is_ascii_digit()) {
let value = literal.digits.parse::<u128>().map_err(|_| {
sema_diag(
"EXPR-PARSE-LOGICAL-LITERAL",
"invalid decimal integral literal",
literal.span,
&["decimal literals must fit in the supported integer range"],
)
})?;
let width = if let Some(width) = literal.width {
width
} else if literal.signed {
decimal_signed_width(value)
} else {
bit_length(value)
};
unsigned_to_bits(value, width)
} else {
return Err(sema_diag(
"EXPR-PARSE-LOGICAL-LITERAL",
"decimal literals cannot use x/z digits",
literal.span,
&["use based literals for unknown digits"],
));
}
}
};
if bits.is_empty() {
bits.push(BoundBit::Zero);
}
if let Some(width) = literal.width {
bits = resize_bits(bits, width, literal.signed);
}
Ok(BoundIntegralValue {
bits,
signed: literal.signed,
})
}
fn decimal_signed_width(value: u128) -> u32 {
if value == 0 {
1
} else {
bit_length(value).saturating_add(1)
}
}
fn bit_length(value: u128) -> u32 {
if value == 0 {
1
} else {
u128::BITS - value.leading_zeros()
}
}
fn unsigned_to_bits(value: u128, width: u32) -> Vec<BoundBit> {
let width = width.max(1);
let mut bits = Vec::with_capacity(width as usize);
for shift in (0..width).rev() {
if shift >= u128::BITS {
bits.push(BoundBit::Zero);
} else if (value >> shift) & 1 == 1 {
bits.push(BoundBit::One);
} else {
bits.push(BoundBit::Zero);
}
}
bits
}
fn resize_bits(mut bits: Vec<BoundBit>, width: u32, signed: bool) -> Vec<BoundBit> {
let target = width.max(1) as usize;
if bits.len() > target {
bits = bits[bits.len() - target..].to_vec();
} else if bits.len() < target {
let fill = if signed {
bits.first().copied().unwrap_or(BoundBit::Zero)
} else {
BoundBit::Zero
};
let mut extended = vec![fill; target - bits.len()];
extended.extend(bits);
bits = extended;
}
bits
}
fn bit_from_char(ch: char) -> Option<BoundBit> {
match ch.to_ascii_lowercase() {
'0' => Some(BoundBit::Zero),
'1' => Some(BoundBit::One),
'x' | 'h' | 'u' | 'w' | 'l' | '-' => Some(BoundBit::X),
'z' => Some(BoundBit::Z),
_ => None,
}
}
fn bits_from_sample(raw: &str) -> Vec<BoundBit> {
raw.chars()
.map(|ch| bit_from_char(ch).unwrap_or(BoundBit::X))
.collect()
}
fn push_hex_nibble(ch: char, out: &mut Vec<BoundBit>) -> Option<()> {
match ch.to_ascii_lowercase() {
'0' => out.extend([
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
]),
'1' => out.extend([
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::One,
]),
'2' => out.extend([
BoundBit::Zero,
BoundBit::Zero,
BoundBit::One,
BoundBit::Zero,
]),
'3' => out.extend([BoundBit::Zero, BoundBit::Zero, BoundBit::One, BoundBit::One]),
'4' => out.extend([
BoundBit::Zero,
BoundBit::One,
BoundBit::Zero,
BoundBit::Zero,
]),
'5' => out.extend([BoundBit::Zero, BoundBit::One, BoundBit::Zero, BoundBit::One]),
'6' => out.extend([BoundBit::Zero, BoundBit::One, BoundBit::One, BoundBit::Zero]),
'7' => out.extend([BoundBit::Zero, BoundBit::One, BoundBit::One, BoundBit::One]),
'8' => out.extend([
BoundBit::One,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
]),
'9' => out.extend([BoundBit::One, BoundBit::Zero, BoundBit::Zero, BoundBit::One]),
'a' => out.extend([BoundBit::One, BoundBit::Zero, BoundBit::One, BoundBit::Zero]),
'b' => out.extend([BoundBit::One, BoundBit::Zero, BoundBit::One, BoundBit::One]),
'c' => out.extend([BoundBit::One, BoundBit::One, BoundBit::Zero, BoundBit::Zero]),
'd' => out.extend([BoundBit::One, BoundBit::One, BoundBit::Zero, BoundBit::One]),
'e' => out.extend([BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::Zero]),
'f' => out.extend([BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::One]),
'x' | 'h' | 'u' | 'w' | 'l' | '-' => {
out.extend([BoundBit::X, BoundBit::X, BoundBit::X, BoundBit::X])
}
'z' => out.extend([BoundBit::Z, BoundBit::Z, BoundBit::Z, BoundBit::Z]),
_ => return None,
}
Some(())
}
fn part_select_width(msb: i64, lsb: i64, span: Span) -> Result<usize, ExprDiagnostic> {
let width = if msb >= lsb {
msb.checked_sub(lsb)
} else {
lsb.checked_sub(msb)
}
.and_then(|delta| delta.checked_add(1))
.ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"part-select bounds overflow supported range",
span,
&["part-select bounds must stay within i64 arithmetic range"],
)
})?;
usize::try_from(width).map_err(|_| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"part-select width exceeds supported range",
span,
&["part-select width must fit in usize"],
)
})
}
fn invalid_part_select_forces_four_state(base_ty: &ExprType, msb: i64, lsb: i64) -> bool {
base_ty.is_four_state || !selection_range_is_in_bounds(base_ty.width, msb, lsb)
}
fn indexed_part_select_may_produce_x(
base_ty: &ExprType,
index_base: &BoundLogicalNode,
width: usize,
up: bool,
) -> Result<bool, ExprDiagnostic> {
if base_ty.is_four_state {
return Ok(true);
}
let Some(start) = try_eval_const_i64(index_base)? else {
return Ok(true);
};
let delta = width as i64 - 1;
let Some(end) = (if up {
start.checked_add(delta)
} else {
start.checked_sub(delta)
}) else {
return Ok(true);
};
Ok(!selection_range_is_in_bounds(base_ty.width, start, end))
}
fn selection_range_is_in_bounds(base_width: u32, first: i64, last: i64) -> bool {
let upper_bound = i64::from(base_width);
first >= 0 && last >= 0 && first < upper_bound && last < upper_bound
}
fn eval_const_i64(
node: &BoundLogicalNode,
context: &str,
span: Span,
) -> Result<i64, ExprDiagnostic> {
let Some(value) = eval_const_node(node)? else {
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-REQUIRED",
"constant integer expression is required",
span,
&[context],
));
};
if value
.bits
.iter()
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z))
{
return Err(sema_diag(
"EXPR-SEMANTIC-CONST-REQUIRED",
"constant integer expression must not contain x/z",
span,
&[context],
));
}
bits_to_i64(&value.bits, value.signed).ok_or_else(|| {
sema_diag(
"EXPR-SEMANTIC-CONST-RANGE",
"constant integer expression is out of range",
span,
&["constant must fit in signed 64-bit range"],
)
})
}
fn try_eval_const_i64(node: &BoundLogicalNode) -> Result<Option<i64>, ExprDiagnostic> {
let Some(value) = eval_const_node(node)? else {
return Ok(None);
};
if value
.bits
.iter()
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z))
{
return Ok(None);
}
Ok(bits_to_i64(&value.bits, value.signed))
}
fn eval_const_node(node: &BoundLogicalNode) -> Result<Option<BoundIntegralValue>, ExprDiagnostic> {
let value = match &node.kind {
BoundLogicalKind::SignalRef { .. } => return Ok(None),
BoundLogicalKind::IntegralLiteral { value, .. } => value.clone(),
BoundLogicalKind::RealLiteral { .. } => return Ok(None),
BoundLogicalKind::StringLiteral { .. } => return Ok(None),
BoundLogicalKind::EnumLabel { value, .. } => value.clone(),
BoundLogicalKind::Parenthesized { expr } => {
let Some(value) = eval_const_node(expr)? else {
return Ok(None);
};
value
}
BoundLogicalKind::Cast { kind, expr } => {
let Some(inner) = eval_const_node(expr)? else {
return Ok(None);
};
if !is_integral_type(&node.ty) {
return Ok(None);
}
apply_const_cast(*kind, inner, &node.ty)
}
BoundLogicalKind::Selection { .. } => return Ok(None),
BoundLogicalKind::Unary { op, expr } => {
let Some(inner) = eval_const_node(expr)? else {
return Ok(None);
};
eval_const_unary(*op, inner, &node.ty)
}
BoundLogicalKind::Binary { op, left, right } => {
let Some(lhs) = eval_const_node(left)? else {
return Ok(None);
};
let Some(rhs) = eval_const_node(right)? else {
return Ok(None);
};
eval_const_binary(*op, lhs, rhs, &node.ty)
}
BoundLogicalKind::Conditional {
condition,
when_true,
when_false,
} => {
let Some(cond) = eval_const_node(condition)? else {
return Ok(None);
};
let Some(lhs) = eval_const_node(when_true)? else {
return Ok(None);
};
let Some(rhs) = eval_const_node(when_false)? else {
return Ok(None);
};
match truthiness_bits(&cond.bits) {
ConstTruth::One => coerce_const_to_type(lhs, &node.ty),
ConstTruth::Zero => coerce_const_to_type(rhs, &node.ty),
ConstTruth::Unknown => {
let lhs = coerce_const_to_type(lhs, &node.ty);
let rhs = coerce_const_to_type(rhs, &node.ty);
let bits = lhs
.bits
.iter()
.zip(rhs.bits.iter())
.map(|(a, b)| if a == b { *a } else { BoundBit::X })
.collect();
BoundIntegralValue {
bits,
signed: node.ty.is_signed,
}
}
}
}
BoundLogicalKind::Inside { .. } => return Ok(None),
BoundLogicalKind::Concatenation { items } => {
let mut bits = Vec::new();
for item in items {
let Some(value) = eval_const_node(item)? else {
return Ok(None);
};
bits.extend(coerce_const_to_type(value, &item.ty).bits);
}
BoundIntegralValue {
bits,
signed: false,
}
}
BoundLogicalKind::Replication { count, expr } => {
let Some(value) = eval_const_node(expr)? else {
return Ok(None);
};
let value = coerce_const_to_type(value, &expr.ty);
let mut bits = Vec::with_capacity(value.bits.len() * *count);
for _ in 0..*count {
bits.extend(value.bits.iter().copied());
}
BoundIntegralValue {
bits,
signed: false,
}
}
BoundLogicalKind::Triggered { .. } => return Ok(None),
};
Ok(Some(coerce_const_to_type(value, &node.ty)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConstTruth {
Zero,
One,
Unknown,
}
fn truthiness_bits(bits: &[BoundBit]) -> ConstTruth {
let mut unknown = false;
for bit in bits {
match bit {
BoundBit::One => return ConstTruth::One,
BoundBit::X | BoundBit::Z => unknown = true,
BoundBit::Zero => {}
}
}
if unknown {
ConstTruth::Unknown
} else {
ConstTruth::Zero
}
}
fn apply_const_cast(
kind: BoundCastKind,
value: BoundIntegralValue,
result_ty: &ExprType,
) -> BoundIntegralValue {
match kind {
BoundCastKind::Signed | BoundCastKind::Unsigned => {
let mut value = value;
value.signed = result_ty.is_signed;
value
}
BoundCastKind::Static => coerce_const_to_type(value, result_ty),
}
}
fn eval_const_unary(
op: UnaryOpAst,
value: BoundIntegralValue,
ty: &ExprType,
) -> BoundIntegralValue {
match op {
UnaryOpAst::Plus => coerce_const_to_type(value, ty),
UnaryOpAst::Minus => {
let value = coerce_const_to_type(value, ty);
if value
.bits
.iter()
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z))
{
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
}
if let Some(raw) = bits_to_u128(&value.bits) {
let modulus = 1_u128.checked_shl(ty.width.min(127)).unwrap_or(0);
let negated = if modulus == 0 {
(!raw).wrapping_add(1)
} else {
modulus.wrapping_sub(raw) & (modulus - 1)
};
BoundIntegralValue {
bits: unsigned_to_bits(negated, ty.width),
signed: ty.is_signed,
}
} else {
BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
}
}
}
UnaryOpAst::LogicalNot => BoundIntegralValue {
bits: vec![match truthiness_bits(&value.bits) {
ConstTruth::Zero => BoundBit::One,
ConstTruth::One => BoundBit::Zero,
ConstTruth::Unknown => BoundBit::X,
}],
signed: false,
},
UnaryOpAst::BitNot => BoundIntegralValue {
bits: coerce_const_to_type(value, ty)
.bits
.into_iter()
.map(|bit| match bit {
BoundBit::Zero => BoundBit::One,
BoundBit::One => BoundBit::Zero,
BoundBit::X | BoundBit::Z => BoundBit::X,
})
.collect(),
signed: ty.is_signed,
},
UnaryOpAst::ReduceAnd
| UnaryOpAst::ReduceNand
| UnaryOpAst::ReduceOr
| UnaryOpAst::ReduceNor
| UnaryOpAst::ReduceXor
| UnaryOpAst::ReduceXnor => {
let value = coerce_const_to_type(value, ty);
let reduced = match op {
UnaryOpAst::ReduceAnd => reduce_and(&value.bits),
UnaryOpAst::ReduceNand => invert_reduce(reduce_and(&value.bits)),
UnaryOpAst::ReduceOr => reduce_or(&value.bits),
UnaryOpAst::ReduceNor => invert_reduce(reduce_or(&value.bits)),
UnaryOpAst::ReduceXor => reduce_xor(&value.bits),
UnaryOpAst::ReduceXnor => invert_reduce(reduce_xor(&value.bits)),
_ => BoundBit::X,
};
BoundIntegralValue {
bits: vec![reduced],
signed: false,
}
}
}
}
fn eval_const_binary(
op: BinaryOpAst,
left: BoundIntegralValue,
right: BoundIntegralValue,
ty: &ExprType,
) -> BoundIntegralValue {
let exponent_signed = right.signed;
let left = coerce_const_to_type(left, ty);
let right = coerce_const_to_type(right, ty);
match op {
BinaryOpAst::Add
| BinaryOpAst::Subtract
| BinaryOpAst::Multiply
| BinaryOpAst::Divide
| BinaryOpAst::Modulo
| BinaryOpAst::Power
| BinaryOpAst::ShiftLeft
| BinaryOpAst::ShiftRight
| BinaryOpAst::ShiftArithLeft
| BinaryOpAst::ShiftArithRight
| BinaryOpAst::BitAnd
| BinaryOpAst::BitXor
| BinaryOpAst::BitXnor
| BinaryOpAst::BitOr => {
if left
.bits
.iter()
.chain(right.bits.iter())
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z))
{
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
}
}
_ => {}
}
match op {
BinaryOpAst::Add => numeric_binary(&left, &right, ty, |a, b| a.wrapping_add(b)),
BinaryOpAst::Subtract => numeric_binary(&left, &right, ty, |a, b| a.wrapping_sub(b)),
BinaryOpAst::Multiply => numeric_binary(&left, &right, ty, |a, b| a.wrapping_mul(b)),
BinaryOpAst::Divide => {
if bits_to_u128(&right.bits) == Some(0) {
BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
}
} else if ty.is_signed {
let Some(lhs) = bits_to_i128(&left.bits, true) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
let Some(rhs) = bits_to_i128(&right.bits, true) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
if rhs == 0 {
BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
}
} else {
BoundIntegralValue {
bits: signed_to_bits(lhs.wrapping_div(rhs), ty.width),
signed: ty.is_signed,
}
}
} else {
numeric_binary(&left, &right, ty, |a, b| a / b)
}
}
BinaryOpAst::Modulo => {
if bits_to_u128(&right.bits) == Some(0) {
BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
}
} else if ty.is_signed {
let Some(lhs) = bits_to_i128(&left.bits, true) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
let Some(rhs) = bits_to_i128(&right.bits, true) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
if rhs == 0 {
BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
}
} else {
BoundIntegralValue {
bits: signed_to_bits(lhs.wrapping_rem(rhs), ty.width),
signed: ty.is_signed,
}
}
} else {
numeric_binary(&left, &right, ty, |a, b| a % b)
}
}
BinaryOpAst::Power => {
let Some(base) = bits_to_u128(&left.bits) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
let Some(exp) = bits_to_u128(&right.bits) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
let acc = if exponent_signed {
let Some(exp_signed) = bits_to_i128(&right.bits, true) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
if exp_signed < 0 {
if base == 0 {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
}
0
} else {
pow_wrapping_u128(base, exp_signed as u128)
}
} else {
pow_wrapping_u128(base, exp)
};
BoundIntegralValue {
bits: unsigned_to_bits(acc, ty.width),
signed: ty.is_signed,
}
}
BinaryOpAst::ShiftLeft | BinaryOpAst::ShiftArithLeft => {
let shift = bits_to_u128(&right.bits)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(usize::MAX);
if shift >= left.bits.len() {
BoundIntegralValue {
bits: vec![BoundBit::Zero; left.bits.len()],
signed: left.signed,
}
} else {
let mut bits = left.bits[shift..].to_vec();
bits.extend(std::iter::repeat_n(BoundBit::Zero, shift));
BoundIntegralValue {
bits,
signed: left.signed,
}
}
}
BinaryOpAst::ShiftRight => {
let shift = bits_to_u128(&right.bits)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(usize::MAX);
if shift >= left.bits.len() {
BoundIntegralValue {
bits: vec![BoundBit::Zero; left.bits.len()],
signed: left.signed,
}
} else {
let mut bits = vec![BoundBit::Zero; shift];
bits.extend(left.bits[..left.bits.len() - shift].iter().copied());
BoundIntegralValue {
bits,
signed: left.signed,
}
}
}
BinaryOpAst::ShiftArithRight => {
let shift = bits_to_u128(&right.bits)
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(usize::MAX);
let fill = if left.signed {
left.bits.first().copied().unwrap_or(BoundBit::Zero)
} else {
BoundBit::Zero
};
if shift >= left.bits.len() {
BoundIntegralValue {
bits: vec![fill; left.bits.len()],
signed: left.signed,
}
} else {
let mut bits = vec![fill; shift];
bits.extend(left.bits[..left.bits.len() - shift].iter().copied());
BoundIntegralValue {
bits,
signed: left.signed,
}
}
}
BinaryOpAst::BitAnd => BoundIntegralValue {
bits: left
.bits
.iter()
.zip(right.bits.iter())
.map(|(lhs, rhs)| bitwise_and(*lhs, *rhs))
.collect(),
signed: ty.is_signed,
},
BinaryOpAst::BitOr => BoundIntegralValue {
bits: left
.bits
.iter()
.zip(right.bits.iter())
.map(|(lhs, rhs)| bitwise_or(*lhs, *rhs))
.collect(),
signed: ty.is_signed,
},
BinaryOpAst::BitXor => BoundIntegralValue {
bits: left
.bits
.iter()
.zip(right.bits.iter())
.map(|(lhs, rhs)| bitwise_xor(*lhs, *rhs))
.collect(),
signed: ty.is_signed,
},
BinaryOpAst::BitXnor => BoundIntegralValue {
bits: left
.bits
.iter()
.zip(right.bits.iter())
.map(|(lhs, rhs)| invert_reduce(bitwise_xor(*lhs, *rhs)))
.collect(),
signed: ty.is_signed,
},
BinaryOpAst::Lt
| BinaryOpAst::Le
| BinaryOpAst::Gt
| BinaryOpAst::Ge
| BinaryOpAst::Eq
| BinaryOpAst::Ne
| BinaryOpAst::CaseEq
| BinaryOpAst::CaseNe
| BinaryOpAst::WildEq
| BinaryOpAst::WildNe
| BinaryOpAst::LogicalAnd
| BinaryOpAst::LogicalOr => BoundIntegralValue {
bits: vec![BoundBit::X],
signed: false,
},
}
}
fn numeric_binary<F>(
left: &BoundIntegralValue,
right: &BoundIntegralValue,
ty: &ExprType,
op: F,
) -> BoundIntegralValue
where
F: Fn(u128, u128) -> u128,
{
let Some(lhs) = bits_to_u128(&left.bits) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
let Some(rhs) = bits_to_u128(&right.bits) else {
return BoundIntegralValue {
bits: vec![BoundBit::X; ty.width as usize],
signed: ty.is_signed,
};
};
BoundIntegralValue {
bits: unsigned_to_bits(op(lhs, rhs), ty.width),
signed: ty.is_signed,
}
}
fn coerce_const_to_type(value: BoundIntegralValue, ty: &ExprType) -> BoundIntegralValue {
let mut bits = resize_bits(value.bits, ty.width, value.signed);
if !ty.is_four_state {
for bit in &mut bits {
if matches!(bit, BoundBit::X | BoundBit::Z) {
*bit = BoundBit::Zero;
}
}
}
BoundIntegralValue {
bits,
signed: ty.is_signed,
}
}
fn bits_to_u128(bits: &[BoundBit]) -> Option<u128> {
let mut value = 0u128;
for bit in bits {
value <<= 1;
match bit {
BoundBit::Zero => {}
BoundBit::One => value |= 1,
BoundBit::X | BoundBit::Z => return None,
}
}
Some(value)
}
fn bits_to_i64(bits: &[BoundBit], signed: bool) -> Option<i64> {
let unsigned = bits_to_u128(bits)?;
if !signed {
return i64::try_from(unsigned).ok();
}
let width = bits.len().min(64);
if width == 0 {
return Some(0);
}
let mask = if width == 64 {
u64::MAX
} else {
(1_u64 << width) - 1
};
let narrowed = (unsigned as u64) & mask;
let signed_value = if width == 64 {
narrowed as i64
} else {
let sign_bit = 1_u64 << (width - 1);
if narrowed & sign_bit == 0 {
narrowed as i64
} else {
let magnitude = ((!narrowed).wrapping_add(1)) & mask;
-(magnitude as i64)
}
};
Some(signed_value)
}
fn bits_to_i128(bits: &[BoundBit], signed: bool) -> Option<i128> {
let unsigned = bits_to_u128(bits)?;
if !signed {
return i128::try_from(unsigned).ok();
}
let width = bits.len().clamp(1, 128);
if width == 128 {
return Some(unsigned as i128);
}
let mask = (1_u128 << width) - 1;
let narrowed = unsigned & mask;
let sign_bit = 1_u128 << (width - 1);
if narrowed & sign_bit == 0 {
Some(narrowed as i128)
} else {
let magnitude = ((!narrowed).wrapping_add(1)) & mask;
Some(-(magnitude as i128))
}
}
fn signed_to_bits(value: i128, width: u32) -> Vec<BoundBit> {
unsigned_to_bits(value as u128, width)
}
fn pow_wrapping_u128(mut base: u128, mut exp: u128) -> u128 {
let mut acc = 1u128;
while exp > 0 {
if exp & 1 == 1 {
acc = acc.wrapping_mul(base);
}
exp >>= 1;
if exp > 0 {
base = base.wrapping_mul(base);
}
}
acc
}
fn reduce_and(bits: &[BoundBit]) -> BoundBit {
let mut unknown = false;
for bit in bits {
match bit {
BoundBit::Zero => return BoundBit::Zero,
BoundBit::One => {}
BoundBit::X | BoundBit::Z => unknown = true,
}
}
if unknown { BoundBit::X } else { BoundBit::One }
}
fn reduce_or(bits: &[BoundBit]) -> BoundBit {
let mut unknown = false;
for bit in bits {
match bit {
BoundBit::One => return BoundBit::One,
BoundBit::Zero => {}
BoundBit::X | BoundBit::Z => unknown = true,
}
}
if unknown { BoundBit::X } else { BoundBit::Zero }
}
fn reduce_xor(bits: &[BoundBit]) -> BoundBit {
if bits
.iter()
.any(|bit| matches!(bit, BoundBit::X | BoundBit::Z))
{
return BoundBit::X;
}
let ones = bits.iter().filter(|bit| **bit == BoundBit::One).count();
if ones % 2 == 0 {
BoundBit::Zero
} else {
BoundBit::One
}
}
fn invert_reduce(bit: BoundBit) -> BoundBit {
match bit {
BoundBit::Zero => BoundBit::One,
BoundBit::One => BoundBit::Zero,
BoundBit::X | BoundBit::Z => BoundBit::X,
}
}
fn bitwise_and(lhs: BoundBit, rhs: BoundBit) -> BoundBit {
match (lhs, rhs) {
(BoundBit::Zero, _) | (_, BoundBit::Zero) => BoundBit::Zero,
(BoundBit::One, BoundBit::One) => BoundBit::One,
_ => BoundBit::X,
}
}
fn bitwise_or(lhs: BoundBit, rhs: BoundBit) -> BoundBit {
match (lhs, rhs) {
(BoundBit::One, _) | (_, BoundBit::One) => BoundBit::One,
(BoundBit::Zero, BoundBit::Zero) => BoundBit::Zero,
_ => BoundBit::X,
}
}
fn bitwise_xor(lhs: BoundBit, rhs: BoundBit) -> BoundBit {
match (lhs, rhs) {
(BoundBit::Zero, BoundBit::Zero) | (BoundBit::One, BoundBit::One) => BoundBit::Zero,
(BoundBit::Zero, BoundBit::One) | (BoundBit::One, BoundBit::Zero) => BoundBit::One,
_ => BoundBit::X,
}
}
fn sema_diag(code: &'static str, message: &str, span: Span, notes: &[&str]) -> ExprDiagnostic {
ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code,
message: message.to_string(),
primary_span: span,
notes: notes.iter().map(|note| (*note).to_string()).collect(),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::expr::parser::parse_logical_expr_ast;
#[derive(Default)]
struct HostStub {
handles: HashMap<String, SignalHandle>,
}
impl HostStub {
fn with_defaults() -> Self {
let mut handles = HashMap::new();
handles.insert("a".to_string(), SignalHandle(1));
handles.insert("b".to_string(), SignalHandle(2));
handles.insert("idx".to_string(), SignalHandle(3));
Self { handles }
}
}
impl ExpressionHost for HostStub {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
self.handles
.get(name)
.copied()
.ok_or_else(|| ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN-SIGNAL",
message: format!("unknown signal '{name}'"),
primary_span: Span::new(0, 0),
notes: vec![],
})
}
fn signal_type(&self, _handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(bit_vector_type(8, true, false, true))
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("0".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
#[test]
fn binder_rejects_unsized_concat_literal() {
let ast = parse_logical_expr_ast("{1, a}").expect("parse");
let host = HostStub::with_defaults();
let error = bind_logical_expr_ast(&ast, &host).expect_err("bind should fail");
assert_eq!(error.code, "EXPR-SEMANTIC-CONCAT-UNSIZED");
}
#[test]
fn binder_rejects_non_constant_replication_multiplier() {
let ast = parse_logical_expr_ast("{idx{a}}").expect("parse");
let host = HostStub::with_defaults();
let error = bind_logical_expr_ast(&ast, &host).expect_err("bind should fail");
assert_eq!(error.code, "EXPR-SEMANTIC-CONST-REQUIRED");
}
#[test]
fn binder_preserves_enum_identity_in_conditional_arms() {
struct EnumHost;
impl ExpressionHost for EnumHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"cond" => Ok(SignalHandle(1)),
"lhs" => Ok(SignalHandle(2)),
"rhs" => Ok(SignalHandle(3)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN",
message: "unknown".to_string(),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
if handle == SignalHandle(1) {
Ok(bit_vector_type(1, true, false, false))
} else {
Ok(ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("fsm_state".to_string()),
enum_labels: None,
})
}
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("0".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
let ast = parse_logical_expr_ast("cond ? lhs : rhs").expect("parse");
let bound = bind_logical_expr_ast(&ast, &EnumHost).expect("bind");
assert!(matches!(bound.root.ty.kind, ExprTypeKind::EnumCore));
assert_eq!(bound.root.ty.enum_type_id.as_deref(), Some("fsm_state"));
}
}