Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

126 changes: 93 additions & 33 deletions crates/celox-frontend-sv/src/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! dependency.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::path::Path;

use celox_design::{
BinaryOp, BitAccess, DomainKind, ExternFunction, ExternSignature, ExternType, InitialStateData,
Expand Down Expand Up @@ -107,8 +107,7 @@ pub(crate) struct LoweredSvModule {
#[derive(Clone)]
struct AnalyzedSvModule {
name: String,
source_code: String,
source_path: PathBuf,
source: std::rc::Rc<sv::ParsedSource>,
implicit_nets_allowed: bool,
/// The positional interface of every module in all sources, used to bind
/// positional port and parameter connections.
Expand Down Expand Up @@ -173,21 +172,34 @@ fn analyze_sources(
sources: &[(&str, &Path)],
) -> Result<HashMap<String, AnalyzedSvModule>, sv::AnalyzerError> {
let mut modules = HashMap::default();
let sources: Vec<_> = sources
.iter()
.map(|(code, path)| {
Ok((
std::rc::Rc::new(sv::ParsedSource::parse(code, path)?),
*code,
*path,
))
})
.collect::<Result<_, sv::AnalyzerError>>()?;
let mut interfaces = sv::ModuleInterfaces::default();
for (code, path) in sources {
interfaces.extend(sv::source_module_interfaces(code, path)?);
for (source, _, _) in &sources {
interfaces.extend(source.module_interfaces().clone());
}
let interfaces = std::sync::Arc::new(interfaces);
let mut packages = HashMap::default();
for (code, path) in sources {
for package in sv::source_packages(code, path)? {
for (source, _, _) in &sources {
for package in source.packages()? {
packages.insert(package.name.clone(), package);
}
}
let packages = std::sync::Arc::new(packages);
for (code, path) in sources {
let implicit_net_permissions = sv::source_module_implicit_net_permissions(code, path)?;
for module_name in sv::source_module_names(code, path)? {
for (source, code, path) in &sources {
let implicit_net_permissions: HashMap<_, _> =
sv::source_module_implicit_net_permissions(code, path)?
.into_iter()
.collect();
for module_name in source.module_names().iter().cloned() {
let name = module_name.clone();
if modules.contains_key(&name) {
return Err(sv::AnalyzerError::DuplicateModule { name: module_name });
Expand All @@ -196,12 +208,11 @@ fn analyze_sources(
name,
AnalyzedSvModule {
implicit_nets_allowed: implicit_net_permissions
.iter()
.find_map(|(name, allowed)| (name == &module_name).then_some(*allowed))
.get(&module_name)
.copied()
.unwrap_or(true),
name: module_name,
source_code: (*code).to_string(),
source_path: (*path).to_path_buf(),
source: source.clone(),
interfaces: interfaces.clone(),
packages: packages.clone(),
},
Expand Down Expand Up @@ -843,26 +854,12 @@ fn specialize_module(
.iter()
.filter_map(|parameter| Some((parameter.name.clone(), parameter.type_text.clone()?)))
.collect();
let typed = sv::apply_module_type_parameters(
&module.source_code,
&module.source_path,
&module.name,
&type_overrides,
)?;
let typed_code = typed.as_deref().unwrap_or(&module.source_code);
// A module that uses packages is analyzed with their items inlined.
let inlined = sv::inline_module_packages(
typed_code,
&module.source_path,
&module.name,
&module.packages,
)?;
let ir = sv::analyze_source_module_with_parameter_expr_overrides(
inlined.as_deref().unwrap_or(typed_code),
&module.source_path,
let ir = module.source.analyze_module(
&module.name,
&overrides,
&type_overrides,
&module.interfaces,
&module.packages,
)?;
let specialized = ir
.modules()
Expand Down Expand Up @@ -1917,6 +1914,62 @@ type SvGlue = (
SLTNodeArena<GlueAddr>,
);

/// The type of `variable` when it is an unpacked array.
fn unpacked_array_type(variable: &SvVariable) -> Option<sv::typecheck::UnpackedArrayType> {
let element_width = unpacked_element_width(variable)?;
Some(sv::typecheck::UnpackedArrayType {
dims: variable.array_dims.clone(),
element_width,
signed: variable.signed,
four_state: variable.is_4state,
})
}

/// Rejects an unpacked array connected to an unpacked array input or output
/// port of an incompatible type: such a connection is an assignment-like
/// context (IEEE 1800-2023 10.8, 23.3.3), which requires equivalent element
/// types and equal element counts (7.6).
fn check_unpacked_port_connection(
connection: &LoweredSvPortConnection,
child: &LoweredSvModule,
parent_variables: &HashMap<SourceVarId, SvVariable>,
parent_signal_names: &HashMap<String, SourceVarId>,
) -> Result<(), ParserError> {
let Some(sv::ir::Expr::Ident(actual)) = connection.actual_expr.as_ref() else {
return Ok(());
};
let Some(port) = child
.signal_names
.get(&connection.formal)
.and_then(|id| child.variables.get(id))
.filter(|port| matches!(port.kind, VariableKind::Input | VariableKind::Output))
else {
return Ok(());
};
let (Some(target), Some(actual_type)) = (
unpacked_array_type(port),
parent_signal_names
.get(actual)
.and_then(|id| parent_variables.get(id))
.and_then(unpacked_array_type),
) else {
return Ok(());
};
if actual_type.is_assignment_compatible_with(&target) {
return Ok(());
}
let error = sv::AnalyzerError::IncompatibleUnpackedArray {
context: format!("connection of port `{}`", connection.formal),
actual: actual_type,
target,
};
Err(ParserError::illegal_context(
"systemverilog port connection",
error.to_string(),
None,
))
}

fn build_instance_glue(
parent_variables: &HashMap<SourceVarId, SvVariable>,
parent_signal_names: &HashMap<String, SourceVarId>,
Expand Down Expand Up @@ -1946,6 +1999,7 @@ fn build_instance_glue(
None,
));
}
check_unpacked_port_connection(connection, child, parent_variables, parent_signal_names)?;
}

for child_port_id in &child.port_order {
Expand Down Expand Up @@ -4960,7 +5014,10 @@ fn sv_glue_expr_is_signed(
sv::ir::Expr::Unary { op, expr } => {
matches!(
op,
sv::ir::UnaryOp::Plus | sv::ir::UnaryOp::Minus | sv::ir::UnaryOp::BitNot
sv::ir::UnaryOp::Plus
| sv::ir::UnaryOp::Minus
| sv::ir::UnaryOp::BitNot
| sv::ir::UnaryOp::ToTwoState
) && sv_glue_expr_is_signed(expr, variables, name_to_id, parameter_types)
}
sv::ir::Expr::Binary { left, op, right } => match op {
Expand Down Expand Up @@ -5019,7 +5076,10 @@ fn sv_expr_is_signed_with_parameters(
sv::ir::Expr::Unary { op, expr } => {
matches!(
op,
sv::ir::UnaryOp::Plus | sv::ir::UnaryOp::Minus | sv::ir::UnaryOp::BitNot
sv::ir::UnaryOp::Plus
| sv::ir::UnaryOp::Minus
| sv::ir::UnaryOp::BitNot
| sv::ir::UnaryOp::ToTwoState
) && sv_expr_is_signed_with_parameters(expr, variables, name_to_id, parameter_types)
}
sv::ir::Expr::Binary { left, op, right } => match op {
Expand Down
23 changes: 21 additions & 2 deletions crates/celox-frontend-sv/src/lowering/comb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2169,7 +2169,7 @@ impl<'p, 'a> Comb<'p, 'a> {
}
let mut probe = store.fork();
let (start, _) = self.eval(&mut probe, frames, &canonical.start, None)?;
let (end, _) = self.eval(&mut probe, frames, &canonical.end, None)?;
let (end, _) = self.loop_end(&mut probe, frames, canonical)?;
let (Some((start, _)), Some((end, _))) = (
slt_const(self.arena, &mut self.consts, start),
slt_const(self.arena, &mut self.consts, end),
Expand All @@ -2187,6 +2187,25 @@ impl<'p, 'a> Comb<'p, 'a> {
Ok(trips > (MAX_UNROLLED_ITERATIONS - self.unrolled.min(MAX_UNROLLED_ITERATIONS)) as f64)
}

/// The bound of a counted loop. It is an operand of the relational loop
/// condition, so it is evaluated at the width of the comparison with the
/// loop variable, and signed only when both operands are (IEEE 1800-2023
/// 11.6.1, 11.8.1 and 11.8.2).
fn loop_end(
&mut self,
store: &mut Store,
frames: &[Frame],
canonical: &CanonicalLoop,
) -> Result<Value, sv::AnalyzerError> {
let var = sv::ir::Expr::Ident(canonical.var.clone());
let signed = self.expr_signed(&var) && self.expr_signed(&canonical.end);
let context = self
.m
.comparison_width(&var, &canonical.end)
.map(|width| (width, signed));
self.eval(store, frames, &canonical.end, context)
}

#[allow(clippy::too_many_arguments)]
fn fold_loop(
&mut self,
Expand Down Expand Up @@ -2215,7 +2234,7 @@ impl<'p, 'a> Comb<'p, 'a> {
let start = coerce_node_width(self.arena, start, Some(loop_width), loop_signed)
.map_err(slt_error)?;
let end_signed = self.expr_signed(&canonical.end);
let (mut end, end_sources) = self.eval(&mut store, frames, &canonical.end, None)?;
let (mut end, end_sources) = self.loop_end(&mut store, frames, canonical)?;
let mut inclusive = matches!(
canonical.compare,
sv::ir::BinaryOp::Le | sv::ir::BinaryOp::Ge
Expand Down
21 changes: 21 additions & 0 deletions crates/celox-frontend-sv/src/lowering/procedural.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,27 @@ impl<'a> ProcModule<'a> {
)
}

/// The width at which `left` and `right` are compared: the larger of
/// their self-determined widths (IEEE 1800-2023 11.6.1 and 11.8.2), with
/// each user function call typed by its declared return type.
pub fn comparison_width(&self, left: &sv::ir::Expr, right: &sv::ir::Expr) -> Option<usize> {
let typed = |expr: &sv::ir::Expr| {
if self.calls(expr) {
self.typed_calls(expr)
} else {
expr.clone()
}
};
sv_comparison_operand_width(
&typed(left),
&typed(right),
self.variables,
self.name_to_id,
self.constants,
self.parameter_types,
)
}

/// `expr` with each user function call replaced by a literal of its
/// return type.
fn typed_calls(&self, expr: &sv::ir::Expr) -> sv::ir::Expr {
Expand Down
4 changes: 4 additions & 0 deletions crates/celox-slt/src/lower/mux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,12 @@ impl SLTToSIRLowerer {
);
}

// An SLT mux is as wide as its wider arm, while a SIR mux needs arms of
// its own width; widen the narrower arm as the folded and CFG paths do.
let then_val = self.lower_inner(builder, then_expr, arena, cache, env, allow_cache);
let then_val = self.cast_reg_width(builder, then_val, res_width);
let else_val = self.lower_inner(builder, else_expr, arena, cache, env, allow_cache);
let else_val = self.cast_reg_width(builder, else_val, res_width);

// Use Mux instruction: preserves Z in 4-state, branchless select in 2-state.
// Backends handle value and mask selection independently.
Expand Down
32 changes: 32 additions & 0 deletions crates/celox-slt/src/lower/tests/mux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,38 @@ fn cheap_mux_stays_branchless() {
);
}

#[test]
fn branchless_mux_widens_the_narrower_arm() {
// An SLT mux is as wide as its wider arm; the SIR mux needs both arms at
// that width.
let mut arena = SLTNodeArena::new();
let cond = input(&mut arena, 0, 1);
let then_expr = input(&mut arena, 1, 32);
let else_expr = constant(&mut arena, 0, 64);
let mux = arena
.alloc(SLTNode::Mux {
cond,
then_expr,
else_expr,
})
.unwrap();
let mut builder = SIRBuilder::new();
let result = SLTToSIRLowerer::new(false).lower(
&mut builder,
mux,
&arena,
&mut crate::HashMap::default(),
);
assert_eq!(builder.register(&result).width(), 64);
let eu = finish_lowering(builder);

assert_eq!(branch_count(&eu), 0);
assert_eq!(
instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
1
);
}

#[test]
fn expected_cost_uses_static_equality_probability() {
let even = StaticBranchProbability::EVEN;
Expand Down
Loading
Loading