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