use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use clap::Args;
use indexmap::{IndexMap, IndexSet};
use miette::{Context, IntoDiagnostic as _};
use tokio::runtime::Runtime;
use crate::Result;
use crate::cmd::sources::get_passed_targets;
use crate::config::{Validate, ValidationContext};
use crate::diagnostic::Warnings;
use crate::sess::{Session, SessionIo};
use crate::src::{SourceFile, SourceGroup, SourceType};
use crate::target::{TargetSet, TargetSpec};
use bender_slang::{SlangPrintOpts, SlangSession, SyntaxTreeRewriter};
#[derive(Args, Debug)]
pub struct PickleArgs {
files: Vec<String>,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
pub target: Vec<String>,
#[arg(short, long)]
pub package: Vec<String>,
#[arg(long)]
pub exclude: Vec<String>,
#[arg(long)]
pub keep_excluded_incdirs: bool,
#[arg(long)]
pub no_deps: bool,
#[arg(short = 'I')]
include_dir: Vec<String>,
#[arg(short = 'D')]
define: Vec<String>,
#[arg(long, help_heading = "Slang Options")]
top: Vec<String>,
#[arg(long, help_heading = "Slang Options", requires = "expand_macros")]
prefix: Option<String>,
#[arg(long, help_heading = "Slang Options", requires = "expand_macros")]
suffix: Option<String>,
#[arg(long, help_heading = "Slang Options")]
exclude_rename: Vec<String>,
#[arg(long, help_heading = "Slang Options")]
expand_macros: bool,
#[arg(long, help_heading = "Slang Options")]
strip_comments: bool,
#[arg(long, help_heading = "Slang Options")]
squash_newlines: bool,
#[arg(long, help_heading = "Slang Options")]
ast_json: bool,
}
pub fn run(sess: &Session, args: PickleArgs) -> Result<()> {
let rt = Runtime::new().into_diagnostic()?;
let io = SessionIo::new(sess);
let srcs = rt.block_on(io.sources(false, &[]))?;
let targets = TargetSet::new(args.target.iter().map(|s| s.as_str()));
let package_set = IndexSet::from_iter(args.package);
let exclude_set = IndexSet::from_iter(args.exclude);
let packages = &srcs.get_package_list(
sess.manifest.package.name.to_string(),
&package_set,
&exclude_set,
args.no_deps,
);
let (targets, packages) = get_passed_targets(sess, &rt, &io, &targets, packages, &package_set)?;
let srcs = srcs
.filter_targets(&targets)
.unwrap_or_default()
.filter_packages(&packages, args.keep_excluded_incdirs)
.unwrap_or_default();
let mut srcs = srcs
.flatten()
.into_iter()
.map(|f| f.validate(&ValidationContext::default()))
.collect::<Result<Vec<_>>>()?;
if !args.files.is_empty() {
let include_dirs = args
.include_dir
.iter()
.map(|d| (TargetSpec::Wildcard, sess.intern_path(Path::new(d))))
.collect();
let defines = args
.define
.iter()
.map(|d| {
let mut parts = d.splitn(2, '=');
let name = parts.next().unwrap_or_default().trim().to_string();
let value = parts
.next()
.map(|v| sess.intern_string(v.trim().to_string()));
(name, (TargetSpec::Wildcard, value))
})
.collect::<IndexMap<_, _>>();
let files = args
.files
.iter()
.map(|f| SourceFile::File(sess.intern_path(Path::new(f)), Some(SourceType::Verilog)))
.collect::<Vec<_>>();
srcs.push(SourceGroup {
include_dirs,
defines,
files,
..SourceGroup::default()
});
}
let print_opts = SlangPrintOpts {
expand_macros: args.expand_macros,
include_directives: !args.expand_macros,
include_comments: !args.strip_comments,
squash_newlines: args.squash_newlines,
};
let mut session = SlangSession::new();
for src_group in srcs {
let include_dirs: Vec<String> = src_group
.include_dirs
.iter()
.chain(src_group.export_incdirs.values().flatten())
.map(|(_, path)| path.to_string_lossy().into_owned())
.chain(args.include_dir.iter().cloned())
.collect::<IndexSet<_>>()
.into_iter()
.collect();
let defines: Vec<String> = src_group
.defines
.iter()
.map(|(def, (_, value))| match value {
Some(v) => format!("{def}={v}"),
None => def.to_string(),
})
.chain(args.define.iter().cloned())
.collect::<IndexSet<_>>()
.into_iter()
.collect();
let file_paths: Vec<String> = src_group
.files
.iter()
.filter_map(|source| match source {
SourceFile::File(path, Some(SourceType::Verilog)) => {
Some(path.to_string_lossy().into_owned())
}
SourceFile::File(path, _) => {
Warnings::PickleNonVerilogFile(path.to_path_buf()).emit();
None
}
_ => unreachable!(),
})
.collect();
session
.parse_group(&file_paths, &include_dirs, &defines)
.into_diagnostic()?;
}
let all_trees = session.all_trees();
let failed: Vec<&str> = all_trees
.iter()
.filter(|t| !t.parsed_ok)
.map(|t| t.path.as_str())
.collect();
if !failed.is_empty() {
return Err(miette::miette!(
"pickle cannot rewrite {} file(s) with slang parse errors (output would be corrupt): {}\n\
see diagnostics above; use `bender script --top ...` if you only need a file list",
failed.len(),
failed.join(", ")
));
}
let trees = if args.top.is_empty() {
all_trees
} else {
session.reachable_trees(&args.top).into_diagnostic()?
};
let mut rewriter = SyntaxTreeRewriter::new();
rewriter.set_prefix(args.prefix.unwrap_or_default());
rewriter.set_suffix(args.suffix.unwrap_or_default());
rewriter.set_excludes(args.exclude_rename);
let trees: Vec<_> = trees
.iter()
.map(|parsed| rewriter.rewrite_declarations(&parsed.tree))
.collect();
let trees: Vec<_> = trees
.iter()
.map(|tree| rewriter.rewrite_references(tree))
.collect();
let raw_writer: Box<dyn Write> = match &args.output {
Some(path) => Box::new(
File::create(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create output file `{path}`"))?,
),
None => Box::new(std::io::stdout()),
};
let mut writer = BufWriter::new(raw_writer);
if args.ast_json {
write!(writer, "[").into_diagnostic()?;
}
let mut first_item = true;
for tree in trees {
if args.ast_json {
if !first_item {
write!(writer, ",").into_diagnostic()?;
}
write!(writer, "{:?}", tree).into_diagnostic()?;
first_item = false;
} else {
write!(writer, "{}", tree.display(print_opts)).into_diagnostic()?;
}
}
if args.ast_json {
writeln!(writer, "]").into_diagnostic()?;
}
Ok(())
}