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,
},
}
impl BoundLogicalKind {
pub(crate) fn direct_signal_handle(&self) -> Option<SignalHandle> {
match self {
Self::SignalRef { handle } | Self::Triggered { handle } => Some(*handle),
_ => None,
}
}
}
#[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)]
#[path = "../tests/sema_const_edges.rs"]
mod sema_const_edges;
#[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 sema_host_stub_exercises_direct_trait_helpers() {
let host = HostStub::with_defaults();
assert_eq!(host.resolve_signal("a").expect("resolve"), SignalHandle(1));
assert!(host.resolve_signal("missing").is_err());
assert!(matches!(
host.sample_value(SignalHandle(1), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(1), 0)
.expect("event check")
);
}
#[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 host = EnumHost;
assert_eq!(
host.resolve_signal("cond").expect("resolve"),
SignalHandle(1)
);
assert!(matches!(
host.sample_value(SignalHandle(2), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(2), 0)
.expect("event check")
);
assert!(host.resolve_signal("missing").is_err());
let ast = parse_logical_expr_ast("cond ? lhs : rhs").expect("parse");
let bound = bind_logical_expr_ast(&ast, &host).expect("bind");
assert!(matches!(bound.root.ty.kind, ExprTypeKind::EnumCore));
assert_eq!(bound.root.ty.enum_type_id.as_deref(), Some("fsm_state"));
}
#[test]
fn event_and_triggered_binding_exercise_event_specific_errors() {
struct MixedHost;
impl ExpressionHost for MixedHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"ev" => Ok(SignalHandle(1)),
"bits" => Ok(SignalHandle(2)),
"real" => Ok(SignalHandle(3)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN",
message: format!("unknown signal '{name}'"),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(1) => ExprType {
kind: ExprTypeKind::Event,
storage: ExprStorage::Scalar,
width: 0,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
},
SignalHandle(3) => real_type(),
_ => bit_vector_type(4, true, false, true),
})
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
unreachable!("semantic binder should not sample values")
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
unreachable!("semantic binder should not query events")
}
}
let host = MixedHost;
assert_eq!(host.resolve_signal("ev").expect("resolve"), SignalHandle(1));
assert!(host.resolve_signal("missing").is_err());
assert!(std::panic::catch_unwind(|| host.sample_value(SignalHandle(1), 0)).is_err());
assert!(std::panic::catch_unwind(|| host.event_occurred(SignalHandle(1), 0)).is_err());
let error = bind_logical_expr_ast(&parse_logical_expr_ast("ev").expect("parse"), &host)
.expect_err("raw event operands require .triggered()");
assert_eq!(error.code, "EXPR-SEMANTIC-EVENT-VALUE");
let bound = bind_logical_expr_ast(
&parse_logical_expr_ast("ev.triggered()").expect("parse"),
&host,
)
.expect("triggered event should bind");
assert!(matches!(
bound.root.kind,
BoundLogicalKind::Triggered {
handle: SignalHandle(1)
}
));
let error = bind_logical_expr_ast(
&parse_logical_expr_ast("bits.triggered()").expect("parse"),
&host,
)
.expect_err("non-event triggered call should fail");
assert_eq!(error.code, "EXPR-SEMANTIC-TRIGGERED");
let error = bind_event_expr_ast(
&crate::expr::parse_event_expr_ast("posedge real").expect("parse"),
&host,
)
.expect_err("edge events require integral operands");
assert_eq!(error.code, "EXPR-SEMANTIC-INTEGRAL-REQUIRED");
}
#[test]
fn type_and_cast_helpers_exercise_incompatible_operands() {
let span = Span::new(2, 5);
assert_eq!(
ensure_integral(&string_type(), span, "ctx")
.expect_err("string is not integral")
.code,
"EXPR-SEMANTIC-INTEGRAL-REQUIRED"
);
assert_eq!(
ensure_numeric(&string_type(), span, "ctx")
.expect_err("string is not numeric")
.code,
"EXPR-SEMANTIC-NUMERIC"
);
assert_eq!(
ensure_boolean_context_type(&string_type(), span, "ctx")
.expect_err("string is not valid in boolean context")
.code,
"EXPR-SEMANTIC-BOOLEAN-CONTEXT"
);
assert_eq!(
ensure_enum_type(&bit_vector_type(2, true, false, true), span, "ctx")
.expect_err("bit vectors are not enums")
.code,
"EXPR-SEMANTIC-ENUM-LABEL"
);
assert_eq!(
ensure_cast_compatible(&string_type(), &ExprTypeKind::BitVector, span)
.expect_err("string cannot target integral casts")
.code,
"EXPR-SEMANTIC-CAST-TARGET"
);
assert_eq!(
ensure_real_cast_source(&string_type(), span)
.expect_err("string cannot cast to real")
.code,
"EXPR-SEMANTIC-CAST-TARGET"
);
assert_eq!(
ensure_string_cast_source(&real_type(), span)
.expect_err("real cannot cast to string")
.code,
"EXPR-SEMANTIC-CAST-TARGET"
);
struct CastHost;
impl ExpressionHost for CastHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"evt" => Ok(SignalHandle(1)),
"enm" => Ok(SignalHandle(2)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN",
message: format!("unknown signal '{name}'"),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(1) => ExprType {
kind: ExprTypeKind::Event,
storage: ExprStorage::Scalar,
width: 0,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
},
_ => ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: None,
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 host = CastHost;
assert_eq!(
host.resolve_signal("evt").expect("resolve"),
SignalHandle(1)
);
assert!(matches!(
host.sample_value(SignalHandle(2), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(2), 0)
.expect("event check")
);
assert!(host.resolve_signal("missing").is_err());
let error = cast_target_type(
&CastTargetAst::RecoveredType {
name: "evt".to_string(),
span,
},
&bit_vector_type(1, true, false, false),
&host,
span,
)
.expect_err("events cannot be recovered cast targets");
assert_eq!(error.code, "EXPR-SEMANTIC-CAST-TARGET");
let error = cast_target_type(
&CastTargetAst::RecoveredType {
name: "enm".to_string(),
span,
},
&bit_vector_type(1, true, false, false),
&host,
span,
)
.expect_err("enum targets require metadata");
assert_eq!(error.code, "EXPR-SEMANTIC-METADATA");
let (ty, kind) = cast_target_type(
&CastTargetAst::Unsigned,
&integer_like_type(IntegerLikeKind::Int),
&CastHost,
span,
)
.expect("unsigned cast should succeed");
assert_eq!(kind, BoundCastKind::Unsigned);
assert!(!ty.is_signed);
assert!(matches!(
common_integral_type(
&integer_like_type(IntegerLikeKind::Int),
&integer_like_type(IntegerLikeKind::Int)
)
.kind,
ExprTypeKind::IntegerLike(IntegerLikeKind::Int)
));
assert!(matches!(
common_numeric_result_type(&real_type(), &bit_vector_type(4, true, false, true)).kind,
ExprTypeKind::Real
));
assert_eq!(
conditional_result_type(&string_type(), span, &real_type(), Span::new(6, 9))
.expect_err("mismatched conditional arms should fail")
.code,
"EXPR-SEMANTIC-CONDITIONAL-TYPE"
);
}
#[test]
fn integral_literal_and_const_helpers_exercise_range_and_unknown_paths() {
let span = Span::new(0, 0);
let binary = decode_integral_literal(&IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Binary,
digits: "10xz".to_string(),
span,
})
.expect("binary literal should decode");
assert_eq!(
binary.bits,
vec![BoundBit::One, BoundBit::Zero, BoundBit::X, BoundBit::Z]
);
let hex = decode_integral_literal(&IntegralLiteral {
width: Some(6),
signed: false,
base: IntegralBase::Hex,
digits: "f".to_string(),
span,
})
.expect("hex literal should decode");
assert_eq!(
hex.bits,
vec![
BoundBit::Zero,
BoundBit::Zero,
BoundBit::One,
BoundBit::One,
BoundBit::One,
BoundBit::One
]
);
assert_eq!(
decode_integral_literal(&IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "12x".to_string(),
span,
})
.expect_err("decimal x digits should fail")
.code,
"EXPR-PARSE-LOGICAL-LITERAL"
);
assert_eq!(
decode_integral_literal(&IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Binary,
digits: "102".to_string(),
span,
})
.expect_err("invalid binary digit should fail")
.code,
"EXPR-PARSE-LOGICAL-LITERAL"
);
assert_eq!(decimal_signed_width(3), 3);
assert_eq!(bit_length(0), 1);
assert_eq!(
resize_bits(vec![BoundBit::One, BoundBit::Zero], 4, true),
vec![BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::Zero]
);
assert_eq!(
resize_bits(vec![BoundBit::One, BoundBit::Zero, BoundBit::One], 2, false),
vec![BoundBit::Zero, BoundBit::One]
);
assert_eq!(bit_from_char('?'), None);
assert_eq!(
bits_from_sample("10q"),
vec![BoundBit::One, BoundBit::Zero, BoundBit::X]
);
let mut nibble = Vec::new();
assert_eq!(push_hex_nibble('g', &mut nibble), None);
let x_node = const_bits_node(&[BoundBit::X], false);
assert_eq!(
eval_const_i64(&x_node, "const", span)
.expect_err("x values are not valid const integers")
.code,
"EXPR-SEMANTIC-CONST-REQUIRED"
);
let signal_node = BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::SignalRef {
handle: SignalHandle(99),
},
};
assert_eq!(
try_eval_const_i64(&signal_node).expect("signal refs are non-constant"),
None
);
assert_eq!(
part_select_width(i64::MAX, i64::MIN, span)
.expect_err("overflowing bounds should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let base_ty = bit_vector_type(4, false, false, true);
assert!(!invalid_part_select_forces_four_state(&base_ty, 2, 1));
assert!(invalid_part_select_forces_four_state(&base_ty, 4, 1));
assert!(
!indexed_part_select_may_produce_x(
&base_ty,
&const_bits_node(&[BoundBit::One], false),
2,
true
)
.expect("in-bounds indexed select")
);
assert!(
indexed_part_select_may_produce_x(
&base_ty,
&const_bits_node(&[BoundBit::One, BoundBit::One], false),
3,
true
)
.expect("out-of-bounds indexed select")
);
let coerced = apply_const_cast(
BoundCastKind::Static,
BoundIntegralValue {
bits: vec![BoundBit::X, BoundBit::One],
signed: false,
},
&bit_vector_type(2, false, false, true),
);
assert_eq!(coerced.bits, vec![BoundBit::Zero, BoundBit::One]);
assert_eq!(
truthiness_bits(&[BoundBit::X, BoundBit::Zero]),
ConstTruth::Unknown
);
let unary = eval_const_unary(
UnaryOpAst::ReduceXnor,
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::One],
signed: false,
},
&bit_vector_type(2, true, false, true),
);
assert_eq!(unary.bits, vec![BoundBit::One]);
let ty = bit_vector_type(4, true, true, true);
let divide_by_zero = eval_const_binary(
BinaryOpAst::Divide,
BoundIntegralValue {
bits: vec![
BoundBit::Zero,
BoundBit::One,
BoundBit::Zero,
BoundBit::Zero,
],
signed: true,
},
BoundIntegralValue {
bits: vec![
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
],
signed: true,
},
&ty,
);
assert_eq!(divide_by_zero.bits, vec![BoundBit::X; 4]);
let neg_power = eval_const_binary(
BinaryOpAst::Power,
BoundIntegralValue {
bits: vec![BoundBit::Zero, BoundBit::Zero],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::One],
signed: true,
},
&bit_vector_type(2, true, true, true),
);
assert_eq!(neg_power.bits, vec![BoundBit::X; 2]);
let shifted = eval_const_binary(
BinaryOpAst::ShiftArithRight,
BoundIntegralValue {
bits: vec![
BoundBit::One,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
],
signed: true,
},
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Zero, BoundBit::Zero],
signed: false,
},
&ty,
);
assert_eq!(shifted.bits, vec![BoundBit::One; 4]);
}
#[test]
fn sema_helpers_exercise_event_success_paths_and_host_detail_errors() {
struct EventHost;
impl ExpressionHost for EventHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"a" => Ok(SignalHandle(1)),
"b" => Ok(SignalHandle(2)),
"c" => Ok(SignalHandle(3)),
"d" => Ok(SignalHandle(4)),
"g" => Ok(SignalHandle(5)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN-SIGNAL",
message: "backend detail".to_string(),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(5) => real_type(),
_ => bit_vector_type(1, true, false, false),
})
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("1".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
let host = EventHost;
assert_eq!(host.resolve_signal("a").expect("resolve"), SignalHandle(1));
assert!(matches!(
host.sample_value(SignalHandle(1), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(1), 0)
.expect("event check")
);
let ast = crate::expr::parse_event_expr_ast("a or posedge b or negedge c or edge d iff g")
.expect("parse");
let bound = bind_event_expr_ast(&ast, &host).expect("bind");
assert!(matches!(
bound.terms[0].event,
BoundEventKind::Named(SignalHandle(1))
));
assert!(matches!(
bound.terms[1].event,
BoundEventKind::Posedge(SignalHandle(2))
));
assert!(matches!(
bound.terms[2].event,
BoundEventKind::Negedge(SignalHandle(3))
));
assert!(matches!(
bound.terms[3].event,
BoundEventKind::Edge(SignalHandle(4))
));
assert!(bound.terms[3].iff.is_some());
let error =
bind_logical_expr_ast(&parse_logical_expr_ast("missing").expect("parse"), &host)
.expect_err("unknown signal should fail");
assert_eq!(error.code, "EXPR-SEMANTIC-UNKNOWN-SIGNAL");
assert!(error.notes[0].contains("backend detail"));
}
#[test]
fn sema_helpers_exercise_canonical_selection_casts_and_binary_typing() {
struct CanonicalHost;
impl ExpressionHost for CanonicalHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"bus[0]" => Ok(SignalHandle(7)),
"real_src" => Ok(SignalHandle(8)),
"string_src" => Ok(SignalHandle(9)),
"enum_src" => Ok(SignalHandle(10)),
other => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: if other == "hostfail[0]" {
"HOST-BROKEN"
} else {
"HOST-UNKNOWN-SIGNAL"
},
message: "lookup failure".to_string(),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(8) => real_type(),
SignalHandle(9) => string_type(),
SignalHandle(10) => ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("state".to_string()),
enum_labels: Some(vec![crate::expr::EnumLabelInfo {
name: "BUSY".to_string(),
bits: "10".to_string(),
}]),
},
_ => bit_vector_type(4, true, false, true),
})
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("1010".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
let host = CanonicalHost;
assert_eq!(
host.resolve_signal("bus[0]").expect("resolve"),
SignalHandle(7)
);
assert!(matches!(
host.sample_value(SignalHandle(7), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(7), 0)
.expect("event check")
);
let bound = bind_logical_expr_ast(&parse_logical_expr_ast("bus[0]").expect("parse"), &host)
.expect("canonical selection should bind through host");
assert!(matches!(
bound.root.kind,
BoundLogicalKind::SignalRef {
handle: SignalHandle(7)
}
));
let ast = parse_logical_expr_ast("hostfail[0]").expect("parse");
let error =
bind_logical_expr_ast(&ast, &CanonicalHost).expect_err("host failure should surface");
assert_eq!(error.code, "HOST-BROKEN");
let span = Span::new(0, 1);
let (ty, kind) = cast_target_type(
&CastTargetAst::Real,
&integer_like_type(IntegerLikeKind::Int),
&CanonicalHost,
span,
)
.expect("real cast should succeed");
assert_eq!(kind, BoundCastKind::Static);
assert!(matches!(ty.kind, ExprTypeKind::Real));
assert!(matches!(
cast_target_type(
&CastTargetAst::RecoveredType {
name: "string_src".to_string(),
span,
},
&string_type(),
&CanonicalHost,
span,
)
.expect("string recovered target should succeed")
.0
.kind,
ExprTypeKind::String
));
assert!(matches!(
cast_target_type(
&CastTargetAst::RecoveredType {
name: "enum_src".to_string(),
span,
},
&bit_vector_type(2, true, false, true),
&CanonicalHost,
span,
)
.expect("enum recovered target should succeed")
.0
.kind,
ExprTypeKind::EnumCore
));
assert!(matches!(bool_result_type().kind, ExprTypeKind::BitVector));
assert_eq!(integer_like_type(IntegerLikeKind::Byte).width, 8);
assert_eq!(integer_like_type(IntegerLikeKind::Shortint).width, 16);
assert_eq!(integer_like_type(IntegerLikeKind::Longint).width, 64);
assert!(integer_like_type(IntegerLikeKind::Integer).is_four_state);
assert!(!integer_like_type(IntegerLikeKind::Time).is_signed);
assert!(matches!(
binary_result_type(
BinaryOpAst::LogicalAnd,
&real_type(),
span,
&bit_vector_type(1, true, false, false),
span,
)
.expect("logical real/integral should typecheck")
.kind,
ExprTypeKind::BitVector
));
assert!(matches!(
binary_result_type(BinaryOpAst::Eq, &string_type(), span, &string_type(), span,)
.expect("string equality should typecheck")
.kind,
ExprTypeKind::BitVector
));
assert_eq!(
binary_result_type(BinaryOpAst::Eq, &string_type(), span, &real_type(), span,)
.expect_err("mixed string equality should fail")
.code,
"EXPR-SEMANTIC-EQUALITY-TYPE"
);
}
#[test]
fn sema_helpers_exercise_enum_lookup_and_low_level_bit_math() {
let span = Span::new(0, 0);
let enum_ty = ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("state".to_string()),
enum_labels: Some(vec![crate::expr::EnumLabelInfo {
name: "IDLE".to_string(),
bits: "00".to_string(),
}]),
};
assert_eq!(
lookup_enum_label_bits(&enum_ty, "IDLE", span).expect("label"),
"00"
);
assert_eq!(
lookup_enum_label_bits(
&ExprType {
enum_labels: None,
..enum_ty.clone()
},
"IDLE",
span
)
.expect_err("missing metadata should fail")
.code,
"EXPR-SEMANTIC-METADATA"
);
assert_eq!(
lookup_enum_label_bits(&enum_ty, "BUSY", span)
.expect_err("unknown label should fail")
.code,
"EXPR-SEMANTIC-ENUM-LABEL"
);
assert!(selection_range_is_in_bounds(4, 0, 3));
assert!(!selection_range_is_in_bounds(4, -1, 3));
assert_eq!(bits_to_u128(&[BoundBit::One, BoundBit::Zero]), Some(2));
assert_eq!(bits_to_i64(&[BoundBit::One, BoundBit::One], true), Some(-1));
assert_eq!(
bits_to_i128(&[BoundBit::One, BoundBit::Zero, BoundBit::One], true),
Some(-3)
);
assert_eq!(signed_to_bits(-1, 2), vec![BoundBit::One, BoundBit::One]);
assert_eq!(pow_wrapping_u128(3, 4), 81);
assert_eq!(reduce_and(&[BoundBit::One, BoundBit::One]), BoundBit::One);
assert_eq!(reduce_or(&[BoundBit::Zero, BoundBit::X]), BoundBit::X);
assert_eq!(reduce_xor(&[BoundBit::One, BoundBit::Zero]), BoundBit::One);
assert_eq!(invert_reduce(BoundBit::Zero), BoundBit::One);
assert_eq!(bitwise_and(BoundBit::One, BoundBit::X), BoundBit::X);
assert_eq!(bitwise_or(BoundBit::Zero, BoundBit::One), BoundBit::One);
assert_eq!(bitwise_xor(BoundBit::One, BoundBit::One), BoundBit::Zero);
}
#[test]
fn bind_logical_node_exercises_manual_ast_variants() {
struct VariantHost;
impl ExpressionHost for VariantHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"vec" => Ok(SignalHandle(1)),
"scalar" => Ok(SignalHandle(2)),
"real" => Ok(SignalHandle(3)),
"str" => Ok(SignalHandle(4)),
"evt" => Ok(SignalHandle(5)),
"state" => Ok(SignalHandle(6)),
"idx" => Ok(SignalHandle(7)),
"vec[0]" => Ok(SignalHandle(8)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN-SIGNAL",
message: "unknown".to_string(),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(2) | SignalHandle(8) => bit_vector_type(1, true, false, false),
SignalHandle(3) => real_type(),
SignalHandle(4) => string_type(),
SignalHandle(5) => ExprType {
kind: ExprTypeKind::Event,
storage: ExprStorage::Scalar,
width: 0,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
},
SignalHandle(6) => ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("state_t".to_string()),
enum_labels: Some(vec![crate::expr::EnumLabelInfo {
name: "BUSY".to_string(),
bits: "10".to_string(),
}]),
},
SignalHandle(7) => integer_like_type(IntegerLikeKind::Int),
_ => bit_vector_type(4, true, false, true),
})
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("1".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
let host = VariantHost;
assert_eq!(
host.resolve_signal("vec").expect("resolve"),
SignalHandle(1)
);
assert!(matches!(
host.sample_value(SignalHandle(1), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(1), 0)
.expect("event check")
);
let span = Span::new(0, 1);
let cases = vec![
LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "5".to_string(),
span,
},
span,
},
LogicalExprNode::RealLiteral {
literal: crate::expr::ast::RealLiteral {
text: "3.5".to_string(),
span,
},
span,
},
LogicalExprNode::StringLiteral {
literal: crate::expr::ast::StringLiteral {
value: "ok".to_string(),
span,
},
span,
},
LogicalExprNode::EnumLabel {
operand: "state".to_string(),
operand_span: span,
label: "BUSY".to_string(),
label_span: span,
span,
},
LogicalExprNode::Parenthesized {
expr: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
span,
},
LogicalExprNode::Cast {
target: CastTargetAst::BitVector {
width: 4,
is_four_state: true,
is_signed: false,
},
expr: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
span,
},
LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::Part {
msb: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "3".to_string(),
span,
},
span,
}),
lsb: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "1".to_string(),
span,
},
span,
}),
},
span,
},
LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::IndexedUp {
base: Box::new(LogicalExprNode::OperandRef {
name: "idx".to_string(),
span,
}),
width: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "2".to_string(),
span,
},
span,
}),
},
span,
},
LogicalExprNode::Unary {
op: UnaryOpAst::Plus,
expr: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
span,
},
LogicalExprNode::Binary {
op: BinaryOpAst::LogicalAnd,
left: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
right: Box::new(LogicalExprNode::OperandRef {
name: "scalar".to_string(),
span,
}),
span,
},
LogicalExprNode::Conditional {
condition: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
when_true: Box::new(LogicalExprNode::OperandRef {
name: "str".to_string(),
span,
}),
when_false: Box::new(LogicalExprNode::StringLiteral {
literal: crate::expr::ast::StringLiteral {
value: "alt".to_string(),
span,
},
span,
}),
span,
},
LogicalExprNode::Inside {
expr: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
set: vec![InsideItemAst::Range {
low: LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
},
high: LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "3".to_string(),
span,
},
span,
},
span,
}],
span,
},
LogicalExprNode::Concatenation {
items: vec![
LogicalExprNode::OperandRef {
name: "scalar".to_string(),
span,
},
LogicalExprNode::OperandRef {
name: "scalar".to_string(),
span,
},
],
span,
},
LogicalExprNode::Replication {
count: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "2".to_string(),
span,
},
span,
}),
expr: Box::new(LogicalExprNode::OperandRef {
name: "scalar".to_string(),
span,
}),
span,
},
LogicalExprNode::Triggered {
expr: Box::new(LogicalExprNode::OperandRef {
name: "evt".to_string(),
span,
}),
span,
},
LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::Bit {
index: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
}),
},
span,
},
];
for node in cases {
bind_logical_node(&node, &VariantHost).expect("node should bind");
}
}
#[test]
fn sema_manual_error_and_const_eval_paths_exercise_helper_errors() {
struct BranchHost;
impl ExpressionHost for BranchHost {
fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
match name {
"vec" => Ok(SignalHandle(1)),
"scalar" => Ok(SignalHandle(2)),
"real" => Ok(SignalHandle(3)),
"evt" => Ok(SignalHandle(4)),
"str" => Ok(SignalHandle(5)),
_ => Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN-SIGNAL",
message: String::new(),
primary_span: Span::new(0, 0),
notes: vec![],
}),
}
}
fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(match handle {
SignalHandle(2) => integer_like_type(IntegerLikeKind::Int),
SignalHandle(3) => real_type(),
SignalHandle(4) => ExprType {
kind: ExprTypeKind::Event,
storage: ExprStorage::Scalar,
width: 0,
is_four_state: false,
is_signed: false,
enum_type_id: None,
enum_labels: None,
},
SignalHandle(5) => string_type(),
_ => bit_vector_type(4, false, false, true),
})
}
fn sample_value(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<crate::expr::SampledValue, ExprDiagnostic> {
Ok(crate::expr::SampledValue::Integral {
bits: Some("0011".to_string()),
label: None,
})
}
fn event_occurred(
&self,
_handle: SignalHandle,
_timestamp: u64,
) -> Result<bool, ExprDiagnostic> {
Ok(false)
}
}
let host = BranchHost;
assert_eq!(
host.resolve_signal("vec").expect("resolve"),
SignalHandle(1)
);
assert!(matches!(
host.sample_value(SignalHandle(1), 0).expect("sample"),
crate::expr::SampledValue::Integral { .. }
));
assert!(
!host
.event_occurred(SignalHandle(1), 0)
.expect("event check")
);
let span = Span::new(4, 9);
let missing = bind_logical_operand_ref("missing", span, &BranchHost)
.expect_err("unknown operands should fail without host notes");
assert!(missing.notes.is_empty());
let missing_event = resolve_event_signal(&BranchHost, "missing", span)
.expect_err("missing event signals should fail without host notes");
assert!(missing_event.notes.is_empty());
assert_eq!(
const_bits_node(&[BoundBit::One], false).span(),
Span::new(0, 0)
);
let invalid_real = LogicalExprNode::RealLiteral {
literal: crate::expr::ast::RealLiteral {
text: "bogus".to_string(),
span,
},
span,
};
assert_eq!(
bind_logical_node(&invalid_real, &BranchHost)
.expect_err("invalid real literals should fail")
.code,
"EXPR-SEMANTIC-REAL-LITERAL"
);
let scalar_select = LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "scalar".to_string(),
span,
}),
selection: SelectionKindAst::Bit {
index: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
}),
},
span,
};
assert_eq!(
bind_logical_node(&scalar_select, &BranchHost)
.expect_err("scalar integral types are not packed vectors")
.code,
"EXPR-SEMANTIC-SELECTION-BASE"
);
let zero_width_select = LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::IndexedUp {
base: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "1".to_string(),
span,
},
span,
}),
width: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
}),
},
span,
};
assert_eq!(
bind_logical_node(&zero_width_select, &BranchHost)
.expect_err("indexed part-select width zero should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let negative_width_select = LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::IndexedDown {
base: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "1".to_string(),
span,
},
span,
}),
width: Box::new(LogicalExprNode::Unary {
op: UnaryOpAst::Minus,
expr: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "1".to_string(),
span,
},
span,
}),
span,
}),
},
span,
};
assert_eq!(
bind_logical_node(&negative_width_select, &BranchHost)
.expect_err("negative indexed widths should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let chained_trigger = LogicalExprNode::Triggered {
expr: Box::new(LogicalExprNode::Triggered {
expr: Box::new(LogicalExprNode::OperandRef {
name: "evt".to_string(),
span,
}),
span,
}),
span,
};
assert_eq!(
bind_logical_node(&chained_trigger, &BranchHost)
.expect_err("chained triggered calls should fail")
.code,
"EXPR-SEMANTIC-TRIGGERED"
);
let selected_trigger = LogicalExprNode::Triggered {
expr: Box::new(LogicalExprNode::Selection {
base: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
selection: SelectionKindAst::Bit {
index: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
}),
},
span,
}),
span,
};
assert_eq!(
bind_logical_node(&selected_trigger, &BranchHost)
.expect_err("selection.triggered() should fail")
.code,
"EXPR-SEMANTIC-TRIGGERED"
);
let empty_concat = LogicalExprNode::Concatenation {
items: vec![],
span,
};
assert_eq!(
bind_logical_node(&empty_concat, &BranchHost)
.expect_err("empty concatenation should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let overflowing_concat = LogicalExprNode::Concatenation {
items: vec![
LogicalExprNode::Cast {
target: CastTargetAst::BitVector {
width: u32::MAX,
is_four_state: false,
is_signed: false,
},
expr: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
span,
},
LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
},
],
span,
};
assert_eq!(
bind_logical_node(&overflowing_concat, &BranchHost)
.expect_err("concat width overflow should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let inside_non_integral = LogicalExprNode::Inside {
expr: Box::new(LogicalExprNode::OperandRef {
name: "str".to_string(),
span,
}),
set: vec![InsideItemAst::Expr(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
})],
span,
};
assert_eq!(
bind_logical_node(&inside_non_integral, &BranchHost)
.expect_err("inside lhs must be integral")
.code,
"EXPR-SEMANTIC-INTEGRAL-REQUIRED"
);
let zero_replication = LogicalExprNode::Replication {
count: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "0".to_string(),
span,
},
span,
}),
expr: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
span,
};
assert_eq!(
bind_logical_node(&zero_replication, &BranchHost)
.expect_err("zero replication counts should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let negative_replication = LogicalExprNode::Replication {
count: Box::new(LogicalExprNode::Unary {
op: UnaryOpAst::Minus,
expr: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: true,
base: IntegralBase::Decimal,
digits: "1".to_string(),
span,
},
span,
}),
span,
}),
expr: Box::new(LogicalExprNode::OperandRef {
name: "vec".to_string(),
span,
}),
span,
};
assert_eq!(
bind_logical_node(&negative_replication, &BranchHost)
.expect_err("negative replication counts should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let overflowing_replication = LogicalExprNode::Replication {
count: Box::new(LogicalExprNode::IntegralLiteral {
literal: IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Decimal,
digits: "2".to_string(),
span,
},
span,
}),
expr: Box::new(LogicalExprNode::Cast {
target: CastTargetAst::BitVector {
width: u32::MAX,
is_four_state: false,
is_signed: false,
},
expr: Box::new(LogicalExprNode::OperandRef {
name: "real".to_string(),
span,
}),
span,
}),
span,
};
assert_eq!(
bind_logical_node(&overflowing_replication, &BranchHost)
.expect_err("replication width overflow should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let ty = bit_vector_type(4, true, true, true);
assert!(
apply_const_cast(
BoundCastKind::Signed,
BoundIntegralValue {
bits: vec![BoundBit::Zero],
signed: false,
},
&ty,
)
.signed
);
assert!(
!apply_const_cast(
BoundCastKind::Unsigned,
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: true,
},
&bit_vector_type(1, true, false, false),
)
.signed
);
assert_eq!(truthiness_bits(&[BoundBit::Zero]), ConstTruth::Zero);
assert_eq!(truthiness_bits(&[BoundBit::One]), ConstTruth::One);
assert_eq!(
eval_const_unary(
UnaryOpAst::LogicalNot,
BoundIntegralValue {
bits: vec![BoundBit::Zero],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::One]
);
assert_eq!(
eval_const_unary(
UnaryOpAst::BitNot,
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Z],
signed: false,
},
&bit_vector_type(2, true, false, true),
)
.bits,
vec![BoundBit::Zero, BoundBit::X]
);
assert_eq!(
eval_const_unary(
UnaryOpAst::Minus,
BoundIntegralValue {
bits: vec![BoundBit::X, BoundBit::One],
signed: false,
},
&bit_vector_type(2, true, false, true),
)
.bits,
vec![BoundBit::X, BoundBit::X]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Add,
BoundIntegralValue {
bits: unsigned_to_bits(3, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(1, 4),
signed: false,
},
&bit_vector_type(4, true, false, true),
)
.bits,
unsigned_to_bits(4, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftRight,
BoundIntegralValue {
bits: unsigned_to_bits(8, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(1, 4),
signed: false,
},
&bit_vector_type(4, true, false, true),
)
.bits,
vec![
BoundBit::Zero,
BoundBit::One,
BoundBit::Zero,
BoundBit::Zero
]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::BitXnor,
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Zero],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::One],
signed: false,
},
&bit_vector_type(2, true, false, true),
)
.bits,
vec![BoundBit::One, BoundBit::Zero]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::LogicalAnd,
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::X]
);
assert_eq!(
numeric_binary(
&BoundIntegralValue {
bits: vec![BoundBit::X],
signed: false,
},
&BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&bit_vector_type(1, true, false, false),
|lhs, rhs| lhs + rhs,
)
.bits,
vec![BoundBit::X]
);
assert_eq!(bits_to_i128(&[BoundBit::One; 128], true), Some(-1));
assert_eq!(reduce_and(&[BoundBit::Zero, BoundBit::One]), BoundBit::Zero);
assert_eq!(reduce_or(&[BoundBit::One, BoundBit::Zero]), BoundBit::One);
assert_eq!(reduce_xor(&[BoundBit::One, BoundBit::One]), BoundBit::Zero);
assert_eq!(invert_reduce(BoundBit::Z), BoundBit::X);
assert_eq!(bitwise_and(BoundBit::Zero, BoundBit::X), BoundBit::Zero);
assert_eq!(bitwise_or(BoundBit::Zero, BoundBit::Zero), BoundBit::Zero);
assert_eq!(bitwise_xor(BoundBit::Zero, BoundBit::X), BoundBit::X);
assert_eq!(
sema_diag("EXPR-TEST", "msg", span, &["note-a", "note-b"]).notes,
vec!["note-a".to_string(), "note-b".to_string()]
);
let conditional_unknown = BoundLogicalNode {
ty: bit_vector_type(2, true, false, true),
span,
kind: BoundLogicalKind::Conditional {
condition: Box::new(const_bits_node(&[BoundBit::X], false)),
when_true: Box::new(const_bits_node(&[BoundBit::One, BoundBit::Zero], false)),
when_false: Box::new(const_bits_node(&[BoundBit::One, BoundBit::One], false)),
},
};
assert_eq!(
eval_const_node(&conditional_unknown)
.expect("conditional eval should succeed")
.expect("conditional should stay constant")
.bits,
vec![BoundBit::One, BoundBit::X]
);
let concat = BoundLogicalNode {
ty: bit_vector_type(2, true, false, true),
span,
kind: BoundLogicalKind::Concatenation {
items: vec![
const_bits_node(&[BoundBit::One], false),
const_bits_node(&[BoundBit::Zero], false),
],
},
};
assert_eq!(
eval_const_node(&concat)
.expect("concat eval should succeed")
.expect("concat should stay constant")
.bits,
vec![BoundBit::One, BoundBit::Zero]
);
let replication = BoundLogicalNode {
ty: bit_vector_type(2, true, false, true),
span,
kind: BoundLogicalKind::Replication {
count: 2,
expr: Box::new(const_bits_node(&[BoundBit::One], false)),
},
};
assert_eq!(
eval_const_node(&replication)
.expect("replication eval should succeed")
.expect("replication should stay constant")
.bits,
vec![BoundBit::One, BoundBit::One]
);
let non_integral_cast = BoundLogicalNode {
ty: string_type(),
span,
kind: BoundLogicalKind::Cast {
kind: BoundCastKind::Static,
expr: Box::new(const_bits_node(&[BoundBit::One], false)),
},
};
assert_eq!(
eval_const_node(&non_integral_cast).expect("string casts should be non-constant"),
None
);
}
#[test]
fn sema_direct_const_binary_helpers_exercise_signed_zero_and_shift_edges() {
let mut nibble = Vec::new();
for ch in ['4', '8', 'f', 'z'] {
push_hex_nibble(ch, &mut nibble).expect("hex digit should decode");
}
assert_eq!(nibble.len(), 16);
assert!(push_hex_nibble('?', &mut nibble).is_none());
let signed_ty = bit_vector_type(4, true, true, true);
assert_eq!(
eval_const_binary(
BinaryOpAst::Divide,
BoundIntegralValue {
bits: signed_to_bits(-4, 4),
signed: true,
},
BoundIntegralValue {
bits: signed_to_bits(0, 4),
signed: true,
},
&signed_ty,
)
.bits,
vec![BoundBit::X; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Modulo,
BoundIntegralValue {
bits: signed_to_bits(-3, 4),
signed: true,
},
BoundIntegralValue {
bits: signed_to_bits(0, 4),
signed: true,
},
&signed_ty,
)
.bits,
vec![BoundBit::X; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Power,
BoundIntegralValue {
bits: unsigned_to_bits(0, 4),
signed: false,
},
BoundIntegralValue {
bits: signed_to_bits(-1, 4),
signed: true,
},
&bit_vector_type(4, true, false, true),
)
.bits,
vec![BoundBit::X; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Power,
BoundIntegralValue {
bits: unsigned_to_bits(2, 4),
signed: false,
},
BoundIntegralValue {
bits: signed_to_bits(-1, 4),
signed: true,
},
&bit_vector_type(4, true, false, true),
)
.bits,
unsigned_to_bits(0, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftLeft,
BoundIntegralValue {
bits: unsigned_to_bits(3, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(9, 4),
signed: false,
},
&bit_vector_type(4, true, false, true),
)
.bits,
vec![BoundBit::Zero; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftArithRight,
BoundIntegralValue {
bits: signed_to_bits(-1, 4),
signed: true,
},
BoundIntegralValue {
bits: unsigned_to_bits(9, 4),
signed: false,
},
&signed_ty,
)
.bits,
vec![BoundBit::One; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Divide,
BoundIntegralValue {
bits: signed_to_bits(-4, 4),
signed: true,
},
BoundIntegralValue {
bits: signed_to_bits(-2, 4),
signed: true,
},
&signed_ty,
)
.bits,
signed_to_bits(2, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Modulo,
BoundIntegralValue {
bits: signed_to_bits(-5, 4),
signed: true,
},
BoundIntegralValue {
bits: signed_to_bits(2, 4),
signed: true,
},
&signed_ty,
)
.bits,
signed_to_bits(-1, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::Power,
BoundIntegralValue {
bits: vec![BoundBit::X, BoundBit::Zero, BoundBit::One, BoundBit::Zero],
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(2, 4),
signed: false,
},
&bit_vector_type(4, true, false, true),
)
.bits,
vec![BoundBit::X; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::CaseEq,
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::X]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::LogicalOr,
BoundIntegralValue {
bits: vec![BoundBit::Zero],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::X]
);
assert_eq!(
numeric_binary(
&BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&BoundIntegralValue {
bits: vec![BoundBit::X],
signed: false,
},
&bit_vector_type(1, true, false, false),
|lhs, rhs| lhs + rhs,
)
.bits,
vec![BoundBit::X]
);
let unsigned_ty = bit_vector_type(4, true, false, true);
for (op, expected) in [
(BinaryOpAst::Subtract, unsigned_to_bits(2, 4)),
(BinaryOpAst::Multiply, unsigned_to_bits(15, 4)),
(BinaryOpAst::Divide, unsigned_to_bits(1, 4)),
(BinaryOpAst::Modulo, unsigned_to_bits(2, 4)),
] {
assert_eq!(
eval_const_binary(
op,
BoundIntegralValue {
bits: unsigned_to_bits(5, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(3, 4),
signed: false,
},
&unsigned_ty,
)
.bits,
expected,
"{op:?}"
);
}
assert_eq!(
eval_const_binary(
BinaryOpAst::Power,
BoundIntegralValue {
bits: unsigned_to_bits(3, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(2, 4),
signed: false,
},
&unsigned_ty,
)
.bits,
unsigned_to_bits(9, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftLeft,
BoundIntegralValue {
bits: unsigned_to_bits(3, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(1, 4),
signed: false,
},
&unsigned_ty,
)
.bits,
unsigned_to_bits(6, 4)
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftRight,
BoundIntegralValue {
bits: unsigned_to_bits(8, 4),
signed: false,
},
BoundIntegralValue {
bits: unsigned_to_bits(9, 4),
signed: false,
},
&unsigned_ty,
)
.bits,
vec![BoundBit::Zero; 4]
);
assert_eq!(
eval_const_binary(
BinaryOpAst::ShiftArithRight,
BoundIntegralValue {
bits: signed_to_bits(-4, 4),
signed: true,
},
BoundIntegralValue {
bits: unsigned_to_bits(1, 4),
signed: false,
},
&signed_ty,
)
.bits,
vec![BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::Zero]
);
for (op, expected) in [
(
BinaryOpAst::BitAnd,
vec![
BoundBit::Zero,
BoundBit::Zero,
BoundBit::One,
BoundBit::Zero,
],
),
(
BinaryOpAst::BitOr,
vec![BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::Zero],
),
(
BinaryOpAst::BitXor,
vec![BoundBit::One, BoundBit::One, BoundBit::Zero, BoundBit::Zero],
),
] {
assert_eq!(
eval_const_binary(
op,
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Zero, BoundBit::One, BoundBit::Zero],
signed: false,
},
BoundIntegralValue {
bits: vec![BoundBit::Zero, BoundBit::One, BoundBit::One, BoundBit::Zero],
signed: false,
},
&unsigned_ty,
)
.bits,
expected,
"{op:?}"
);
}
assert_eq!(
eval_const_unary(
UnaryOpAst::Plus,
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&unsigned_ty,
)
.bits,
vec![
BoundBit::Zero,
BoundBit::Zero,
BoundBit::Zero,
BoundBit::One
]
);
assert_eq!(
eval_const_unary(
UnaryOpAst::LogicalNot,
BoundIntegralValue {
bits: vec![BoundBit::One],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::Zero]
);
assert_eq!(
eval_const_unary(
UnaryOpAst::LogicalNot,
BoundIntegralValue {
bits: vec![BoundBit::X],
signed: false,
},
&bit_vector_type(1, true, false, false),
)
.bits,
vec![BoundBit::X]
);
for (op, expected) in [
(UnaryOpAst::ReduceAnd, BoundBit::X),
(UnaryOpAst::ReduceNand, BoundBit::X),
(UnaryOpAst::ReduceOr, BoundBit::One),
(UnaryOpAst::ReduceNor, BoundBit::Zero),
(UnaryOpAst::ReduceXor, BoundBit::X),
] {
assert_eq!(
eval_const_unary(
op,
BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::X],
signed: false,
},
&bit_vector_type(2, true, false, true),
)
.bits,
vec![expected],
"{op:?}"
);
}
assert_eq!(bits_to_i64(&[], true), Some(0));
assert_eq!(bits_to_i64(&[BoundBit::One; 64], true), Some(-1));
assert_eq!(
bits_to_i128(&[BoundBit::One, BoundBit::Zero], false),
Some(2)
);
assert_eq!(reduce_and(&[BoundBit::X, BoundBit::One]), BoundBit::X);
assert_eq!(reduce_or(&[BoundBit::X, BoundBit::Zero]), BoundBit::X);
assert_eq!(reduce_xor(&[BoundBit::X, BoundBit::Zero]), BoundBit::X);
assert_eq!(bitwise_and(BoundBit::One, BoundBit::One), BoundBit::One);
assert_eq!(bitwise_or(BoundBit::X, BoundBit::Zero), BoundBit::X);
}
#[test]
fn sema_type_result_helpers_exercise_fallback_numeric_and_unknown_signal_paths() {
let span = Span::new(0, 1);
let enum_ty = ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("state".to_string()),
enum_labels: Some(vec![crate::expr::EnumLabelInfo {
name: "BUSY".to_string(),
bits: "10".to_string(),
}]),
};
let widened = common_integral_type(
&integer_like_type(IntegerLikeKind::Int),
&bit_vector_type(64, false, false, true),
);
assert!(matches!(widened.kind, ExprTypeKind::BitVector));
assert_eq!(widened.width, 64);
assert!(!widened.is_signed);
assert!(!widened.is_four_state);
let integral_numeric = common_numeric_result_type(
&bit_vector_type(8, false, false, true),
&bit_vector_type(4, true, true, true),
);
assert!(matches!(integral_numeric.kind, ExprTypeKind::BitVector));
assert_eq!(integral_numeric.width, 8);
assert!(!integral_numeric.is_signed);
assert!(integral_numeric.is_four_state);
assert!(matches!(
conditional_result_type(
&real_type(),
span,
&integer_like_type(IntegerLikeKind::Byte),
Span::new(2, 3),
)
.expect("real/integral conditionals should coerce to real")
.kind,
ExprTypeKind::Real
));
assert!(matches!(
binary_result_type(
BinaryOpAst::CaseEq,
&bit_vector_type(1, true, false, false),
span,
&bit_vector_type(1, false, false, false),
span,
)
.expect("case equality should produce booleans")
.kind,
ExprTypeKind::BitVector
));
let shifted = binary_result_type(
BinaryOpAst::ShiftRight,
&enum_ty,
span,
&bit_vector_type(1, false, false, false),
span,
)
.expect("integral shifts should succeed for enum-typed lhs");
assert!(matches!(shifted.kind, ExprTypeKind::BitVector));
assert_eq!(shifted.width, 2);
struct EmptyMessageHost;
impl ExpressionHost for EmptyMessageHost {
fn resolve_signal(&self, _name: &str) -> Result<SignalHandle, ExprDiagnostic> {
Err(ExprDiagnostic {
layer: DiagnosticLayer::Semantic,
code: "HOST-UNKNOWN-SIGNAL",
message: String::new(),
primary_span: Span::new(0, 1),
notes: vec![],
})
}
fn signal_type(&self, _handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
Ok(bit_vector_type(1, true, false, false))
}
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 missing = bind_logical_expr_ast(
&parse_logical_expr_ast("missing").expect("parse"),
&EmptyMessageHost,
)
.expect_err("missing signal should fail without host detail notes");
assert!(missing.notes.is_empty());
let triggered = bind_logical_expr_ast(
&parse_logical_expr_ast("missing.triggered()").expect("parse"),
&EmptyMessageHost,
)
.expect_err("missing triggered operand should fail without host detail notes");
assert!(triggered.notes.is_empty());
let cast_error = cast_target_type(
&CastTargetAst::RecoveredType {
name: "missing".to_string(),
span,
},
&bit_vector_type(1, true, false, false),
&EmptyMessageHost,
span,
)
.expect_err("missing recovered cast target should fail");
assert!(cast_error.notes.is_empty());
assert!(matches!(
EmptyMessageHost.signal_type(SignalHandle(1)).unwrap().kind,
ExprTypeKind::BitVector
));
assert!(matches!(
EmptyMessageHost.sample_value(SignalHandle(1), 0).unwrap(),
crate::expr::SampledValue::Integral { .. }
));
assert!(!EmptyMessageHost.event_occurred(SignalHandle(1), 0).unwrap());
}
#[test]
fn sema_literal_decoder_and_const_wrappers_exercise_non_constant_paths() {
let span = Span::new(1, 4);
assert_eq!(
part_select_width(1, 3, span).expect("ascending widths are valid"),
3
);
assert!(
try_eval_const_i64(&const_bits_node(&[BoundBit::X], false))
.expect("x-valued constants should not error")
.is_none()
);
assert_eq!(
eval_const_i64(&const_bits_node(&[BoundBit::One; 128], false), "ctx", span)
.expect_err("oversized constants should fail")
.code,
"EXPR-SEMANTIC-CONST-RANGE"
);
let invalid_hex = crate::expr::ast::IntegralLiteral {
width: Some(4),
signed: false,
base: crate::expr::ast::IntegralBase::Hex,
digits: "?".to_string(),
span,
};
assert_eq!(
decode_integral_literal(&invalid_hex)
.expect_err("invalid hex digits should fail")
.code,
"EXPR-PARSE-LOGICAL-LITERAL"
);
let invalid_decimal = crate::expr::ast::IntegralLiteral {
width: None,
signed: true,
base: crate::expr::ast::IntegralBase::Decimal,
digits: "340282366920938463463374607431768211456".to_string(),
span,
};
assert_eq!(
decode_integral_literal(&invalid_decimal)
.expect_err("decimal overflow should fail")
.code,
"EXPR-PARSE-LOGICAL-LITERAL"
);
let signal = BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::SignalRef {
handle: SignalHandle(9),
},
};
for node in [
BoundLogicalNode {
ty: signal.ty.clone(),
span,
kind: BoundLogicalKind::Parenthesized {
expr: Box::new(signal.clone()),
},
},
BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::Cast {
kind: BoundCastKind::Static,
expr: Box::new(signal.clone()),
},
},
BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::Unary {
op: UnaryOpAst::BitNot,
expr: Box::new(signal.clone()),
},
},
BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::Binary {
op: BinaryOpAst::Add,
left: Box::new(signal.clone()),
right: Box::new(const_bits_node(&[BoundBit::One], false)),
},
},
BoundLogicalNode {
ty: bit_vector_type(1, true, false, false),
span,
kind: BoundLogicalKind::Conditional {
condition: Box::new(signal.clone()),
when_true: Box::new(const_bits_node(&[BoundBit::One], false)),
when_false: Box::new(const_bits_node(&[BoundBit::Zero], false)),
},
},
BoundLogicalNode {
ty: bit_vector_type(2, true, false, true),
span,
kind: BoundLogicalKind::Concatenation {
items: vec![signal.clone(), const_bits_node(&[BoundBit::One], false)],
},
},
BoundLogicalNode {
ty: bit_vector_type(2, true, false, true),
span,
kind: BoundLogicalKind::Replication {
count: 2,
expr: Box::new(signal.clone()),
},
},
] {
assert_eq!(
eval_const_node(&node).expect("non-constant wrappers should not error"),
None
);
}
}
#[test]
fn sema_decoder_and_const_helpers_exercise_empty_digits_enum_constants_and_extra_nibbles() {
let span = Span::new(2, 4);
let zero = decode_integral_literal(&IntegralLiteral {
width: None,
signed: false,
base: IntegralBase::Hex,
digits: String::new(),
span,
})
.expect("empty digit strings should normalize to zero");
assert_eq!(zero.bits, vec![BoundBit::Zero]);
let widened = unsigned_to_bits(1, 130);
assert_eq!(widened.len(), 130);
assert_eq!(widened.first(), Some(&BoundBit::Zero));
assert_eq!(widened.last(), Some(&BoundBit::One));
assert_eq!(
widened.iter().filter(|bit| **bit == BoundBit::One).count(),
1
);
let mut nibble = Vec::new();
for ch in ['5', '6', 'a', 'e', 'x', 'z'] {
push_hex_nibble(ch, &mut nibble).expect("hex nibble should decode");
}
assert_eq!(nibble.len(), 24);
assert_eq!(
&nibble[0..4],
&[BoundBit::Zero, BoundBit::One, BoundBit::Zero, BoundBit::One]
);
assert_eq!(
&nibble[4..8],
&[BoundBit::Zero, BoundBit::One, BoundBit::One, BoundBit::Zero]
);
assert_eq!(
&nibble[8..12],
&[BoundBit::One, BoundBit::Zero, BoundBit::One, BoundBit::Zero]
);
assert_eq!(
&nibble[12..16],
&[BoundBit::One, BoundBit::One, BoundBit::One, BoundBit::Zero]
);
assert_eq!(
&nibble[16..20],
&[BoundBit::X, BoundBit::X, BoundBit::X, BoundBit::X]
);
assert_eq!(
&nibble[20..24],
&[BoundBit::Z, BoundBit::Z, BoundBit::Z, BoundBit::Z]
);
let enum_ty = ExprType {
kind: ExprTypeKind::EnumCore,
storage: ExprStorage::Scalar,
width: 2,
is_four_state: true,
is_signed: false,
enum_type_id: Some("state".to_string()),
enum_labels: Some(vec![crate::expr::EnumLabelInfo {
name: "BUSY".to_string(),
bits: "10".to_string(),
}]),
};
let enum_node = BoundLogicalNode {
ty: enum_ty.clone(),
span,
kind: BoundLogicalKind::EnumLabel {
value: BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Zero],
signed: false,
},
label: "BUSY".to_string(),
},
};
assert_eq!(
eval_const_node(&enum_node).expect("enum labels should be constant"),
Some(BoundIntegralValue {
bits: vec![BoundBit::One, BoundBit::Zero],
signed: false,
})
);
}
fn const_bits_node(bits: &[BoundBit], signed: bool) -> BoundLogicalNode {
BoundLogicalNode {
ty: bit_vector_type(bits.len() as u32, true, signed, bits.len() > 1),
span: Span::new(0, 0),
kind: BoundLogicalKind::IntegralLiteral {
value: BoundIntegralValue {
bits: bits.to_vec(),
signed,
},
is_unsized: false,
},
}
}
}