use crate::run::usage;
use voltaic_lattice::walk;
use voltaic_parser::parse_source;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SigArgs {
pub(crate) inputs: Vec<PathBuf>,
pub(crate) why: Option<String>,
pub(crate) verify: bool,
pub(crate) emit: bool,
pub(crate) require: bool,
pub(crate) against: Option<PathBuf>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Agreement {
pub(crate) checked: usize,
pub(crate) agreed: usize,
pub(crate) differed: Vec<(String, String, String)>,
}
impl Agreement {
pub(crate) fn new() -> Self {
Agreement {
checked: 0,
agreed: 0,
differed: Vec::new(),
}
}
pub(crate) fn absorb(&mut self, other: &Agreement) {
self.checked += other.checked;
self.agreed += other.agreed;
for row in other.differed.iter() {
self.differed.push(row.clone());
}
();
}
}
pub(crate) fn parse_sig_args(args: &[String]) -> Result<SigArgs, String> {
let mut inputs = Vec::new();
let mut why = None;
let mut verify = false;
let mut emit = false;
let mut require = false;
let mut against = None;
let mut index = 0;
while index < args.len() {
let arg = &args[index];
if arg == "--require-signatures" {
require = true;
index += 1;
} else if arg == "--emit" {
emit = true;
index += 1;
} else if arg == "--semver" {
if index + 1 >= args.len() {
return Err("error: `--semver` needs the sidecar to compare against".to_string());
}
against = Some(PathBuf::from(&args[index + 1]));
index += 2;
} else if arg == "--verify" {
verify = true;
index += 1;
} else if arg == "--why" {
if index + 1 >= args.len() {
return Err("error: `--why` needs the name of a definition".to_string());
}
why = Some(args[index + 1].clone());
index += 2;
} else {
if arg.starts_with('-') {
let help = usage();
return Err(format!("error: unknown flag `{}`\n\n{}", arg, help));
}
inputs.push(PathBuf::from(arg));
index += 1;
}
}
if inputs.is_empty() {
let help = usage();
return Err(format!("error: sig needs at least one .vlt\n\n{}", help));
}
Ok(SigArgs {
inputs,
why,
verify,
emit,
require,
against,
})
}
pub(crate) fn sig_command(args: &[String]) -> Result<(), String> {
let parsed = parse_sig_args(args)?;
if let Some(older) = parsed.against {
if parsed.inputs.len() != 1 {
return Err("error: `--semver` compares one .vlt against one sidecar".to_string());
}
return semver(&parsed.inputs[0], &older);
}
if parsed.require {
let mut missing = 0;
for input in parsed.inputs.iter() {
missing += require_one(input)?;
}
if missing == 0 {
println!("every published definition carries a written signature");
return Ok(());
}
return Err(format!(
"error: {} published definition(s) have no written signature",
missing
));
}
if parsed.emit {
for input in parsed.inputs.iter() {
emit_one(input)?;
}
return Ok(());
}
if parsed.verify {
let mut total = Agreement::new();
for input in parsed.inputs.iter() {
let found = verify_one(input)?;
total.absorb(&found);
}
for row in total.differed.iter() {
println!(
"differs: {}\n written {}\n inferred {}",
row.0, row.1, row.2
);
}
println!(
"verified {}/{} written signatures",
total.agreed, total.checked
);
return Ok(());
}
for input in parsed.inputs.iter() {
report(input, parsed.why.as_deref())?;
}
Ok(())
}
pub(crate) fn sidecar_path(input: &Path) -> PathBuf {
let mut shown = input.as_os_str().to_os_string();
shown.push(".sig");
PathBuf::from(shown)
}
pub(crate) fn built(input: &PathBuf) -> Result<voltaic_lattice::Sidecar, String> {
let shown = input.display().to_string();
let source =
std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
let program =
parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
let found = walk(&program);
let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
Ok(voltaic_lattice::of_report(&found, &outcome))
}
pub(crate) fn write_sidecar(input: &PathBuf) -> Result<(), String> {
let sidecar = built(input)?;
let target = sidecar_path(input);
let text = voltaic_lattice::render_sidecar(&sidecar);
std::fs::write(&target, text)
.map_err(|e| format!("error: cannot write {}: {}", target.display(), e))?;
Ok(())
}
fn emit_one(input: &PathBuf) -> Result<(), String> {
let sidecar = built(input)?;
write_sidecar(input)?;
let target = sidecar_path(input);
println!("{}: {} published", target.display(), sidecar.len());
Ok(())
}
fn require_one(input: &PathBuf) -> Result<usize, String> {
let shown = input.display().to_string();
let source =
std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
let program =
parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
let found = walk(&program);
let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
let missing = voltaic_lattice::unwritten(&found, &outcome);
for one in missing.iter() {
let at = voltaic_diag::line_col(&source, one.span.0);
eprintln!(
"{}:{}:{}: `{}` is published with no written signature",
shown, at.0, at.1, one.name
);
eprintln!(" write {} : {}", one.name, one.inferred);
}
Ok(missing.len())
}
fn semver(input: &PathBuf, older: &PathBuf) -> Result<(), String> {
let recorded = std::fs::read_to_string(older)
.map_err(|e| format!("error: cannot read {}: {}", older.display(), e))?;
let before = voltaic_lattice::read_sidecar(&recorded);
let after = built(input)?;
let changes = voltaic_lattice::compare(&before, &after);
if changes.is_empty() {
println!("no API change");
return Ok(());
}
for change in changes.iter() {
println!("{}", change);
}
let count = voltaic_lattice::breaking(changes.as_slice());
if count == 0 {
println!("{} change(s), none breaking", changes.len());
Ok(())
} else {
Err(format!(
"error: {} breaking change(s) of {}",
count,
changes.len()
))
}
}
fn self_free(
written: &voltaic_lattice::Ty,
definition: &voltaic_lattice::Inferred,
) -> voltaic_lattice::Ty {
if let Some(target) = definition.owner.as_deref() {
return voltaic_lattice::with_owner(written, target);
}
written.clone()
}
fn verify_one(input: &PathBuf) -> Result<Agreement, String> {
let shown = input.display().to_string();
let source =
std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
let program =
parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
let found = walk(&program);
let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
let mut tally = Agreement::new();
for definition in found.definitions.iter() {
if let Some(written) = definition.written.as_ref() {
tally.checked += 1;
let inferred = definition.judged(&outcome);
let expanded = found.known.expand(&self_free(written, definition));
if voltaic_lattice::congruent(&inferred, &expanded) {
tally.agreed += 1;
} else {
tally.differed.push((
format!("{}: {}", shown, definition.name),
expanded.to_string(),
inferred.to_string(),
));
}
}
}
Ok(tally)
}
fn report(input: &PathBuf, why: Option<&str>) -> Result<(), String> {
let shown = input.display().to_string();
let source =
std::fs::read_to_string(input).map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
let program =
parse_source(&source).map_err(|e| format!("error: {} does not parse: {:?}", shown, e))?;
let found = walk(&program);
let outcome = voltaic_lattice::solve_with(&found.store, &found.known);
for definition in found.definitions.iter() {
let resolved = definition.judged(&outcome);
let shown_row = if definition.row.is_empty() {
String::new()
} else {
format!(" ! {}", definition.row)
};
let shown_modes = if definition.modes.is_empty() {
String::new()
} else {
let every: Vec<String> = definition
.modes
.iter()
.map(|m| format!("{} {}", m.0, m.1))
.collect();
format!(" @ {}", every.as_slice().join(", "))
};
println!(
"{} : {}{}{}",
definition.name, resolved, shown_row, shown_modes
);
}
if let Some(name) = why {
if let Some(definition) = found.find(name) {
println!("---");
println!("why {} : {}", definition.name, definition.judged(&outcome));
for at in touching(definition, &found.store) {
print!("{}", voltaic_lattice::rendered(at, &found.store, &source));
}
} else {
return Err(format!(
"error: {} has no definition called `{}`",
shown, name
));
}
}
let conflicting = outcome.contradictions(&found.store);
for failure in conflicting.iter() {
let held = found.store.at(failure.at).unwrap();
eprintln!(
"error: {}: {}",
shown,
voltaic_lattice::summarise(&held.obligation)
);
if why.is_none() {
eprint!(
"{}",
voltaic_lattice::rendered(failure.at, &found.store, &source)
);
}
}
let undischarged = outcome.failures.len() - conflicting.len();
if undischarged > 0 {
eprintln!(
"note: {} obligation(s) Lattice could not discharge, which is a gap in Lattice rather than in {}",
undischarged, shown
);
}
if conflicting.is_empty() {
Ok(())
} else {
Err(format!(
"error: {}: {} contradiction(s)",
shown,
conflicting.len()
))
}
}
fn touching(definition: &voltaic_lattice::Inferred, store: &voltaic_lattice::Store) -> Vec<usize> {
let wanted = definition.ty.holes();
let mut hits = Vec::new();
for constraint in store.all().iter().enumerate() {
for hole in constraint.1.holes() {
if wanted.contains(&hole) && !hits.contains(&constraint.0) {
hits.push(constraint.0);
}
}
}
hits
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn sig_refuses_an_empty_argument_list() {
let parsed = parse_sig_args(&Vec::new());
assert!(parsed.is_err());
assert!(parsed.unwrap_err().contains("at least one"));
}
#[test]
fn sig_takes_one_or_more_paths() {
let args = vec!["a.vlt".to_string(), "b.vlt".to_string()];
let parsed = parse_sig_args(args.as_slice()).expect("should parse");
assert_eq!(parsed.inputs.len(), 2);
assert_eq!(parsed.why, None);
}
#[test]
fn why_takes_the_name_after_it() {
let args = vec!["a.vlt".to_string(), "--why".to_string(), "peek".to_string()];
let parsed = parse_sig_args(args.as_slice()).expect("should parse");
assert_eq!(parsed.why, Some("peek".to_string()));
assert_eq!(parsed.inputs.len(), 1);
}
#[test]
fn why_without_a_name_is_an_error_rather_than_a_silent_no_op() {
let args = vec!["a.vlt".to_string(), "--why".to_string()];
let parsed = parse_sig_args(args.as_slice());
assert!(parsed.is_err());
assert!(parsed.unwrap_err().contains("needs the name"));
}
#[test]
fn an_unknown_flag_is_refused_rather_than_taken_as_a_path() {
let args = vec!["--nope".to_string()];
let parsed = parse_sig_args(args.as_slice());
assert!(parsed.is_err());
assert!(parsed.unwrap_err().contains("unknown flag"));
}
}