use clap::ArgMatches;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::process;
use std::process::Command;
use xlsynth::mangle_dslx_name;
use xlsynth_pir::{ir::PackageMember, ir_parser};
pub const DEFAULT_WARNINGS_AS_ERRORS: bool = true;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum PipelineSpec {
Stages(u64),
ClockPeriodPs(u64),
}
pub fn parse_bool_flag(matches: &ArgMatches, flag_name: &str) -> Option<bool> {
matches
.get_one::<String>(flag_name)
.map(|value| value == "true")
}
pub fn parse_bool_flag_or(matches: &ArgMatches, flag_name: &str, default_value: bool) -> bool {
parse_bool_flag(matches, flag_name).unwrap_or(default_value)
}
pub fn enforce_extern_verilog_codegen_policy(
ir_text: &str,
allow_extern_verilog: bool,
) -> Result<(), String> {
if allow_extern_verilog {
return Ok(());
}
let mut parser = ir_parser::Parser::new(ir_text);
let package = parser.parse_package().map_err(|err| {
format!(
"could not inspect IR for extern Verilog declarations while --allow_extern_verilog=false: {err}"
)
})?;
let ffi_members = package
.members
.iter()
.filter_map(|member| {
let function = match member {
PackageMember::Function(function) => function,
PackageMember::Block { func, .. } => func,
};
function
.outer_attrs
.iter()
.any(|attr| attr.trim_start().starts_with("#[ffi_proto("))
.then(|| function.name.clone())
})
.collect::<Vec<_>>();
if ffi_members.is_empty() {
Ok(())
} else {
Err(format!(
"IR contains Verilog FFI declaration(s) via #[ffi_proto(...)] on: {}. Pass --allow_extern_verilog=true to permit codegen of DSLX extern_verilog / Verilog FFI IR.",
ffi_members.join(", ")
))
}
}
pub fn resolve_type_inference_v2(
matches: &ArgMatches,
config: &Option<crate::toolchain_config::ToolchainConfig>,
) -> Option<bool> {
parse_bool_flag(matches, "type_inference_v2").or_else(|| {
config
.as_ref()
.and_then(|c| c.dslx.as_ref()?.type_inference_v2)
})
}
pub fn extract_pipeline_spec(matches: &ArgMatches) -> PipelineSpec {
if let Some(pipeline_stages) = matches.get_one::<String>("pipeline_stages") {
PipelineSpec::Stages(pipeline_stages.parse().unwrap())
} else if let Some(clock_period_ps) = matches.get_one::<String>("clock_period_ps") {
PipelineSpec::ClockPeriodPs(clock_period_ps.parse().unwrap())
} else {
eprintln!("Must provide either --pipeline_stages or --clock_period_ps");
process::exit(1)
}
}
pub fn write_stdout_line(line: &str) {
let mut out = std::io::stdout().lock();
if let Err(e) = writeln!(out, "{}", line) {
if e.kind() == std::io::ErrorKind::BrokenPipe {
process::exit(0);
}
eprintln!("error: failed to write stdout: {e}");
process::exit(1);
}
}
pub fn write_stdout(text: &str) {
let mut out = std::io::stdout().lock();
if let Err(e) = write!(out, "{}", text) {
if e.kind() == std::io::ErrorKind::BrokenPipe {
process::exit(0);
}
eprintln!("error: failed to write stdout: {e}");
process::exit(1);
}
}
#[derive(Debug, Clone)]
pub struct CodegenFlags {
pub input_valid_signal: Option<String>,
pub output_valid_signal: Option<String>,
pub use_system_verilog: Option<bool>,
pub flop_inputs: Option<bool>,
pub flop_outputs: Option<bool>,
pub add_idle_output: Option<bool>,
pub add_invariant_assertions: Option<bool>,
pub module_name: Option<String>,
pub array_index_bounds_checking: Option<bool>,
pub separate_lines: Option<bool>,
pub reset: Option<String>,
pub reset_active_low: Option<bool>,
pub reset_asynchronous: Option<bool>,
pub reset_data_path: Option<bool>,
pub gate_format: Option<String>,
pub assert_format: Option<String>,
pub output_schedule_path: Option<String>,
pub output_verilog_line_map_path: Option<String>,
pub output_block_ir_path: Option<String>,
pub output_residual_data_path: Option<String>,
pub reference_residual_data_path: Option<String>,
}
pub fn extract_codegen_flags(
matches: &ArgMatches,
toolchain_config: Option<&crate::toolchain_config::ToolchainConfig>,
) -> CodegenFlags {
let (gate_format, assert_format) = if let Some(config) = toolchain_config {
(
config.codegen.as_ref().and_then(|c| c.gate_format.clone()),
config
.codegen
.as_ref()
.and_then(|c| c.assert_format.clone()),
)
} else {
(None, None)
};
let mut flags = CodegenFlags {
input_valid_signal: matches
.get_one::<String>("input_valid_signal")
.map(|s| s.to_string()),
output_valid_signal: matches
.get_one::<String>("output_valid_signal")
.map(|s| s.to_string()),
use_system_verilog: matches
.get_one::<String>("use_system_verilog")
.map(|s| s == "true")
.or_else(|| toolchain_config.and_then(|c| c.codegen.as_ref()?.use_system_verilog)),
flop_inputs: matches
.get_one::<String>("flop_inputs")
.map(|s| s == "true"),
flop_outputs: matches
.get_one::<String>("flop_outputs")
.map(|s| s == "true"),
add_idle_output: matches
.get_one::<String>("add_idle_output")
.map(|s| s == "true"),
add_invariant_assertions: matches
.get_one::<String>("add_invariant_assertions")
.map(|s| s == "true")
.or_else(|| {
toolchain_config
.and_then(|c| c.codegen.as_ref())
.and_then(|cg| cg.add_invariant_assertions)
}),
module_name: matches
.get_one::<String>("module_name")
.map(|s| s.to_string()),
array_index_bounds_checking: matches
.get_one::<String>("array_index_bounds_checking")
.map(|s| s == "true"),
separate_lines: matches
.get_one::<String>("separate_lines")
.map(|s| s == "true"),
reset: matches.get_one::<String>("reset").map(|s| s.to_string()),
reset_active_low: matches
.get_one::<String>("reset_active_low")
.map(|s| s == "true"),
reset_asynchronous: matches
.get_one::<String>("reset_asynchronous")
.map(|s| s == "true"),
reset_data_path: matches
.get_one::<String>("reset_data_path")
.map(|s| s == "true"),
gate_format,
assert_format,
output_schedule_path: matches
.get_one::<String>("output_schedule_path")
.map(|s| s.to_string()),
output_verilog_line_map_path: matches
.get_one::<String>("output_verilog_line_map_path")
.map(|s| s.to_string()),
output_block_ir_path: matches
.get_one::<String>("output_block_ir_path")
.map(|s| s.to_string()),
output_residual_data_path: matches
.get_one::<String>("output_residual_data_path")
.map(|s| s.to_string()),
reference_residual_data_path: matches
.get_one::<String>("reference_residual_data_path")
.map(|s| s.to_string()),
};
if flags.use_system_verilog.is_none() {
flags.use_system_verilog = Some(crate::flag_defaults::CODEGEN_USE_SYSTEM_VERILOG);
}
if flags.flop_inputs.is_none() {
flags.flop_inputs = Some(crate::flag_defaults::CODEGEN_FLOP_INPUTS);
}
if flags.flop_outputs.is_none() {
flags.flop_outputs = Some(crate::flag_defaults::CODEGEN_FLOP_OUTPUTS);
}
if flags.add_idle_output.is_none() {
flags.add_idle_output = Some(crate::flag_defaults::CODEGEN_ADD_IDLE_OUTPUT);
}
if flags.add_invariant_assertions.is_none() {
flags.add_invariant_assertions =
Some(crate::flag_defaults::CODEGEN_ADD_INVARIANT_ASSERTIONS);
}
if flags.array_index_bounds_checking.is_none() {
flags.array_index_bounds_checking =
Some(crate::flag_defaults::CODEGEN_ARRAY_INDEX_BOUNDS_CHECKING);
}
flags
}
pub fn codegen_flags_to_textproto(codegen_flags: &CodegenFlags) -> String {
log::debug!(
"codegen_flags_to_textproto; codegen_flags: {:?}",
codegen_flags
);
let mut pieces = vec![];
if let Some(input_valid_signal) = &codegen_flags.input_valid_signal {
pieces.push(format!("input_valid_signal: \"{input_valid_signal}\""));
}
if let Some(output_valid_signal) = &codegen_flags.output_valid_signal {
pieces.push(format!("output_valid_signal: \"{output_valid_signal}\""));
}
if let Some(use_system_verilog) = codegen_flags.use_system_verilog {
pieces.push(format!("use_system_verilog: {use_system_verilog}"));
}
if let Some(flop_inputs) = codegen_flags.flop_inputs {
pieces.push(format!("flop_inputs: {flop_inputs}"));
}
if let Some(flop_outputs) = codegen_flags.flop_outputs {
pieces.push(format!("flop_outputs: {flop_outputs}"));
}
if let Some(add_idle_output) = codegen_flags.add_idle_output {
pieces.push(format!("add_idle_output: {add_idle_output}"));
}
if let Some(add_invariant_assertions) = codegen_flags.add_invariant_assertions {
pieces.push(format!(
"add_invariant_assertions: {add_invariant_assertions}"
));
}
if let Some(module_name) = &codegen_flags.module_name {
pieces.push(format!("module_name: \"{module_name}\""));
}
if let Some(array_index_bounds_checking) = codegen_flags.array_index_bounds_checking {
pieces.push(format!(
"array_index_bounds_checking: {array_index_bounds_checking}"
));
}
if let Some(separate_lines) = codegen_flags.separate_lines {
pieces.push(format!("separate_lines: {separate_lines}"));
}
if let Some(reset) = &codegen_flags.reset {
pieces.push(format!("reset: \"{reset}\""));
}
if let Some(reset_active_low) = codegen_flags.reset_active_low {
pieces.push(format!("reset_active_low: {reset_active_low}"));
}
if let Some(reset_asynchronous) = codegen_flags.reset_asynchronous {
pieces.push(format!("reset_asynchronous: {reset_asynchronous}"));
}
if let Some(reset_data_path) = codegen_flags.reset_data_path {
pieces.push(format!("reset_data_path: {reset_data_path}"));
}
if let Some(gate_format) = &codegen_flags.gate_format {
pieces.push(format!("gate_format: {gate_format:?}"));
}
if let Some(assert_format) = &codegen_flags.assert_format {
pieces.push(format!("assert_format: {assert_format:?}"));
}
if let Some(output_schedule_path) = &codegen_flags.output_schedule_path {
pieces.push(format!("output_schedule_path: \"{output_schedule_path}\""));
}
if let Some(output_verilog_line_map_path) = &codegen_flags.output_verilog_line_map_path {
pieces.push(format!(
"output_verilog_line_map_path: \"{output_verilog_line_map_path}\""
));
}
if let Some(output_block_ir_path) = &codegen_flags.output_block_ir_path {
pieces.push(format!("output_block_ir_path: \"{output_block_ir_path}\""));
}
if let Some(output_residual_data_path) = &codegen_flags.output_residual_data_path {
pieces.push(format!(
"output_residual_data_path: \"{output_residual_data_path}\""
));
}
if let Some(reference_residual_data_path) = &codegen_flags.reference_residual_data_path {
pieces.push(format!(
"reference_residual_data_path: \"{reference_residual_data_path}\""
));
}
pieces.push(format!("assertion_macro_names: \"ASSERT_ON\""));
pieces.join("\n")
}
pub fn add_codegen_flags(command: &mut Command, codegen_flags: &CodegenFlags) {
log::info!("add_codegen_flags");
if let Some(use_system_verilog) = codegen_flags.use_system_verilog {
command.arg(format!("--use_system_verilog={use_system_verilog}"));
}
if let Some(input_valid_signal) = &codegen_flags.input_valid_signal {
command.arg("--input_valid_signal").arg(input_valid_signal);
}
if let Some(output_valid_signal) = &codegen_flags.output_valid_signal {
command
.arg("--output_valid_signal")
.arg(output_valid_signal);
}
if let Some(flop_inputs) = codegen_flags.flop_inputs {
command.arg(format!("--flop_inputs={flop_inputs}"));
}
if let Some(flop_outputs) = codegen_flags.flop_outputs {
command.arg(format!("--flop_outputs={flop_outputs}"));
}
if let Some(add_idle_output) = codegen_flags.add_idle_output {
command.arg(format!("--add_idle_output={add_idle_output}"));
}
if let Some(add_invariant_assertions) = codegen_flags.add_invariant_assertions {
command.arg(format!(
"--add_invariant_assertions={add_invariant_assertions}"
));
}
if let Some(module_name) = &codegen_flags.module_name {
command.arg("--module_name").arg(module_name);
}
if let Some(array_index_bounds_checking) = codegen_flags.array_index_bounds_checking {
command.arg(format!(
"--array_index_bounds_checking={array_index_bounds_checking}"
));
}
if let Some(separate_lines) = codegen_flags.separate_lines {
command.arg(format!("--separate_lines={separate_lines}"));
}
if let Some(reset) = &codegen_flags.reset {
command.arg(format!("--reset={reset}"));
}
if let Some(reset_active_low) = codegen_flags.reset_active_low {
command.arg(format!("--reset_active_low={reset_active_low}"));
}
if let Some(reset_asynchronous) = codegen_flags.reset_asynchronous {
command.arg(format!("--reset_asynchronous={reset_asynchronous}"));
}
if let Some(reset_data_path) = codegen_flags.reset_data_path {
command.arg(format!("--reset_data_path={reset_data_path}"));
}
if let Some(gate_format) = &codegen_flags.gate_format {
command.arg(format!("--gate_format={gate_format}"));
}
if let Some(assert_format) = &codegen_flags.assert_format {
command.arg(format!("--assert_format={assert_format}"));
}
if let Some(output_schedule_path) = &codegen_flags.output_schedule_path {
command
.arg("--output_schedule_path")
.arg(output_schedule_path);
}
if let Some(output_verilog_line_map_path) = &codegen_flags.output_verilog_line_map_path {
command
.arg("--output_verilog_line_map_path")
.arg(output_verilog_line_map_path);
}
if let Some(output_block_ir_path) = &codegen_flags.output_block_ir_path {
command.arg(format!("--output_block_ir_path={output_block_ir_path}"));
}
if let Some(output_residual_data_path) = &codegen_flags.output_residual_data_path {
command.arg(format!(
"--output_residual_data_path={output_residual_data_path}"
));
}
if let Some(reference_residual_data_path) = &codegen_flags.reference_residual_data_path {
command.arg(format!(
"--reference_residual_data_path={reference_residual_data_path}"
));
}
}
pub fn scheduling_options_proto(delay_model: &str, pipeline_spec: &PipelineSpec) -> String {
let mut lines = vec![format!("delay_model: \"{}\"", delay_model)];
match pipeline_spec {
PipelineSpec::Stages(stages) => lines.push(format!("pipeline_stages: {}", stages)),
PipelineSpec::ClockPeriodPs(clock_period_ps) => {
lines.push(format!("clock_period_ps: {}", clock_period_ps))
}
}
lines.join("\n")
}
pub fn pipeline_codegen_flags_proto(codegen_flags: &CodegenFlags) -> String {
format!(
"register_merge_strategy: STRATEGY_IDENTITY_ONLY\ngenerator: GENERATOR_KIND_PIPELINE\n{}",
codegen_flags_to_textproto(codegen_flags)
)
}
pub fn collect_dslx_search_paths(
matches: &ArgMatches,
config: &Option<crate::toolchain_config::ToolchainConfig>,
) -> Vec<std::path::PathBuf> {
let mut out: Vec<std::path::PathBuf> = Vec::new();
if let Some(flag_value) = matches.get_one::<String>("dslx_path") {
for entry in flag_value.split(';').filter(|s| !s.is_empty()) {
out.push(std::path::PathBuf::from(entry));
}
}
if let Some(cfg) = config {
if let Some(dslx_cfg) = &cfg.dslx {
if let Some(vec) = &dslx_cfg.dslx_path {
for p in vec {
if !p.is_empty() {
out.push(std::path::PathBuf::from(p));
}
}
}
}
}
let mut seen: HashSet<std::path::PathBuf> = HashSet::new();
out.retain(|p| seen.insert(p.clone()));
out
}
pub struct DslxPaths {
pub stdlib_path: Option<std::path::PathBuf>,
pub search_paths: Vec<std::path::PathBuf>,
}
impl DslxPaths {
pub fn search_path_views(&self) -> Vec<&std::path::Path> {
self.search_paths.iter().map(|p| p.as_path()).collect()
}
}
pub fn get_dslx_paths(
matches: &ArgMatches,
config: &Option<crate::toolchain_config::ToolchainConfig>,
) -> DslxPaths {
use crate::toolchain_config::get_dslx_stdlib_path;
let stdlib_path_opt =
get_dslx_stdlib_path(matches, config).map(|s| std::path::PathBuf::from(s));
let search_paths = collect_dslx_search_paths(matches, config);
DslxPaths {
stdlib_path: stdlib_path_opt,
search_paths,
}
}
pub fn find_and_verify_executable(
name: &str,
install_hint: &str,
) -> anyhow::Result<std::path::PathBuf> {
let exe_path = which::which(name).map_err(|_| {
anyhow::anyhow!("`{}` executable not found in PATH. {}", name, install_hint)
})?;
if let Ok(metadata) = std::fs::metadata(&exe_path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = metadata.permissions();
if perms.mode() & 0o111 == 0 {
return Err(anyhow::anyhow!(
"{} binary at '{}' exists but is not executable (permissions: {:o})",
name,
exe_path.display(),
perms.mode()
));
}
}
}
Ok(exe_path)
}
pub fn execute_command_with_context(
mut cmd: std::process::Command,
context: &str,
) -> anyhow::Result<std::process::Output> {
log::debug!("execute_command_with_context: About to execute command");
let result = cmd.output().map_err(|e| {
anyhow::anyhow!(
"{}: {}. This could indicate missing dynamic libraries or other execution issues.",
context,
e
)
});
match &result {
Ok(output) => log::debug!(
"execute_command_with_context: Command completed with status: {}",
output.status
),
Err(e) => log::debug!("execute_command_with_context: Command failed: {}", e),
}
result
}
use xlsynth_prover::prover::types::ParamDomains;
pub fn get_enum_domain(
tcm: &xlsynth::dslx::TypecheckedModule,
enum_def: &xlsynth::dslx::EnumDef,
) -> Vec<xlsynth::IrValue> {
let mut values = Vec::new();
for mi in 0..enum_def.get_member_count() {
let m = enum_def.get_member(mi);
let expr = m.get_value();
let owner_module = expr.get_owner_module();
let owner_type_info = tcm
.get_type_info_for_module(&owner_module)
.expect("imported type info");
let interp = owner_type_info.get_const_expr(&expr).expect("constexpr");
let ir_val = interp.convert_to_ir().expect("convert");
values.push(ir_val);
}
values
}
pub fn get_function_enum_param_domains(
tcm: &xlsynth::dslx::TypecheckedModule,
dslx_top: &str,
) -> Result<ParamDomains, String> {
let module = tcm.get_module();
let type_info = tcm.get_type_info();
let mut domains: ParamDomains = std::collections::HashMap::new();
let mut found = false;
for i in 0..module.get_member_count() {
if let Some(xlsynth::dslx::MatchableModuleMember::Function(f)) =
module.get_member(i).to_matchable()
{
if f.get_identifier() == dslx_top {
found = true;
for pidx in 0..f.get_param_count() {
let p = f.get_param(pidx);
let name = p.get_name();
let ta = p.get_type_annotation();
let ty = type_info.get_type_for_type_annotation(&ta).unwrap();
if ty.is_enum() {
let enum_def = ty.get_enum_def().unwrap();
let values = get_enum_domain(tcm, &enum_def);
domains.insert(name, values);
}
}
}
}
}
if found {
Ok(domains)
} else {
Err(format!(
"Function '{}' not found in module '{}': available members: {}",
dslx_top,
module.get_name(),
(0..module.get_member_count())
.filter_map(|idx| module.get_member(idx).to_matchable())
.filter_map(|member| match member {
xlsynth::dslx::MatchableModuleMember::Function(func) =>
Some(func.get_identifier()),
_ => None,
})
.collect::<Vec<_>>()
.join(", ")
))
}
}
pub fn parse_uf_spec(
module_name: &str,
matches: Option<clap::parser::ValuesRef<String>>,
) -> HashMap<String, String> {
let mut m = std::collections::HashMap::new();
if let Some(vals) = matches {
for v in vals {
if let Some((fn_name, uf_name)) = v.split_once(':') {
m.insert(
mangle_dslx_name(module_name, fn_name.trim()).unwrap(),
uf_name.trim().to_string(),
);
} else {
eprintln!(
"Error: invalid uninterpreted function specification '{}'; expected <function-name>:<uf-name>",
v
);
std::process::exit(1);
}
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
const IR_WITH_VERILOG_FFI: &str = r#"package extern_verilog_policy
#[ffi_proto("""code_template: "assign {return} = {x};"
""")]
fn verilog_passthrough(x: bits[8] id=1) -> bits[8] {
ret x: bits[8] = param(name=x, id=1)
}
top fn main(x: bits[8] id=2) -> bits[8] {
ret x: bits[8] = param(name=x, id=2)
}
"#;
const IR_WITHOUT_VERILOG_FFI: &str = r#"package ordinary_codegen
top fn main(x: bits[8] id=1) -> bits[8] {
ret x: bits[8] = param(name=x, id=1)
}
"#;
#[test]
fn extern_verilog_codegen_policy_accepts_ordinary_ir() {
enforce_extern_verilog_codegen_policy(IR_WITHOUT_VERILOG_FFI, false)
.expect("ordinary IR should pass policy check");
}
#[test]
fn extern_verilog_codegen_policy_rejects_ffi_ir_when_disabled() {
let err = enforce_extern_verilog_codegen_policy(IR_WITH_VERILOG_FFI, false)
.expect_err("Verilog FFI IR should require explicit opt-in");
assert!(err.contains("verilog_passthrough"), "{err}");
assert!(err.contains("--allow_extern_verilog=true"), "{err}");
}
#[test]
fn extern_verilog_codegen_policy_accepts_ffi_ir_when_enabled() {
enforce_extern_verilog_codegen_policy(IR_WITH_VERILOG_FFI, true)
.expect("explicit opt-in should allow Verilog FFI IR");
}
#[test]
fn test_get_function_enum_param_domains_cross_module() {
use xlsynth::dslx;
let tmpdir = xlsynth_test_helpers::make_test_tmpdir("xlsynth_driver_dslx_test");
let dir = tmpdir.path();
let imported_path = dir.join("imported.x");
let imported_dslx = r#"
const K = u3:5;
pub enum ImpE : u3 { Z = 0, P = K }
"#;
std::fs::write(&imported_path, imported_dslx).expect("write imported.x");
let main_path = dir.join("main.x");
let main_dslx = r#"
import imported;
pub fn top(x: imported::ImpE) -> u3 { u3:0 }
"#;
std::fs::write(&main_path, main_dslx).expect("write main.x");
let mut import_data = dslx::ImportData::new(None, &[dir]);
let tcm = dslx::parse_and_typecheck(
main_dslx,
main_path.to_str().unwrap(),
"main",
&mut import_data,
)
.expect("parse_and_typecheck success");
let domains = get_function_enum_param_domains(&tcm, "top").expect("function exists");
assert!(domains.contains_key("x"));
let values = domains.get("x").unwrap();
assert_eq!(values.len(), 2);
assert!(values.contains(&xlsynth::IrValue::make_ubits(3, 0).unwrap()));
assert!(values.contains(&xlsynth::IrValue::make_ubits(3, 5).unwrap()));
}
}