voltaic-cli 0.2.0

The Voltaic compiler: .vlt in, .vrs containing ordinary Rust out.
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)